From 696e1d8cf068397b5b2a4d2995f376c0326d520e Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Wed, 22 Jul 2026 01:04:03 -0400 Subject: [PATCH 01/20] Make the build and Core compile on Linux (X11) Phase 0 (build system): gate NVRHI's D3D11/D3D12 projects and links to Windows, use VK_USE_PLATFORM_XLIB_KHR and exclude dxgi-format.cpp on Linux; scope DirectXCompiler/WinSock/WinMM/WinVersion/Bcrypt to Windows in Dependencies.lua; fix NVRHI/ImGuizmo include casing and the Dbghelp tab; drop duplicate ShaderC/ShaderCUtil/STB keys; add PIC filters to msdf-atlas-gen; host-guard the Editor pkg-config call. Phase 1 (Core source): remove the "Linux is not supported" #error; implement ThreadSignal via std::mutex/condition_variable with correct auto/manual-reset semantics; give GetPersistentStoragePath an XDG-based Linux path; repair the Linux HLSL dxc shell-out (syntax, nvrhi::ShaderType stage flags, ShaderStageToString) and hash the source in the preprocess no-op so the shader cache invalidates correctly; exclude HlslIncluder.cpp on Linux (no libdxcompiler link); add a GCC breakpoint in Assert.h; silence the Memory.h non-Windows warning. Scripting: make ScriptBuilder invoke dotnet directly instead of via a Windows-only PowerShell wrapper. The nvrhi and msdf-atlas-gen premake edits live in those submodules and must be committed to their forks separately. Windows build is unaffected: every source change is inside a Linux branch, a Linux-only file, or the non-Windows branch of a shared header. Co-Authored-By: Claude Opus 4.8 --- Core/Platform/Linux/LinuxFileSystem.cpp | 14 ++++- Core/Platform/Linux/LinuxThread.cpp | 62 ++++++++++++++++++- Core/Source/Lux/Core/Assert.h | 4 ++ Core/Source/Lux/Core/Memory.h | 3 +- Core/Source/Lux/Core/PlatformDetection.h | 1 - .../ShaderCompiler/VulkanShaderCompiler.cpp | 14 +++-- Core/Source/Lux/Scripting/ScriptBuilder.cpp | 10 +-- Core/premake5.lua | 5 ++ Dependencies.lua | 33 +++------- Editor/premake5.lua | 6 +- premake5.lua | 2 +- 11 files changed, 110 insertions(+), 44 deletions(-) diff --git a/Core/Platform/Linux/LinuxFileSystem.cpp b/Core/Platform/Linux/LinuxFileSystem.cpp index 064bc09a..11fc262b 100644 --- a/Core/Platform/Linux/LinuxFileSystem.cpp +++ b/Core/Platform/Linux/LinuxFileSystem.cpp @@ -76,11 +76,19 @@ namespace Lux { if (!s_PersistentStoragePath.empty()) return s_PersistentStoragePath; - s_PersistentStoragePath = HasEnvironmentVariable("HAZEL_DIR") ? GetEnvironmentVariable("HAZEL_DIR") : ".."; - s_PersistentStoragePath /= "Hazelnut"; + // Follow the XDG Base Directory spec: $XDG_DATA_HOME, else ~/.local/share. Mirrors the + // Windows implementation, which roots persistent data at %APPDATA%/Editor. + if (HasEnvironmentVariable("XDG_DATA_HOME")) + s_PersistentStoragePath = GetEnvironmentVariable("XDG_DATA_HOME"); + else if (HasEnvironmentVariable("HOME")) + s_PersistentStoragePath = std::filesystem::path(GetEnvironmentVariable("HOME")) / ".local" / "share"; + else + s_PersistentStoragePath = ".."; + + s_PersistentStoragePath /= "Editor"; if (!std::filesystem::exists(s_PersistentStoragePath)) - std::filesystem::create_directory(s_PersistentStoragePath); + std::filesystem::create_directories(s_PersistentStoragePath); return s_PersistentStoragePath; } diff --git a/Core/Platform/Linux/LinuxThread.cpp b/Core/Platform/Linux/LinuxThread.cpp index 577e0cdb..e6530ae9 100644 --- a/Core/Platform/Linux/LinuxThread.cpp +++ b/Core/Platform/Linux/LinuxThread.cpp @@ -3,6 +3,9 @@ #include +#include +#include + namespace Lux { Thread::Thread(const std::string& name) @@ -12,15 +15,68 @@ namespace Lux { void Thread::SetName(const std::string& name) { - pthread_setname_np(m_Thread.native_handle(), name.c_str()); + // pthread limits thread names to 16 bytes (including the null terminator). + pthread_setname_np(m_Thread.native_handle(), name.substr(0, 15).c_str()); } void Thread::Join() { - m_Thread.join(); + if (m_Thread.joinable()) + m_Thread.join(); + } + + // Linux replacement for the Win32 named-event ThreadSignal. Mirrors the auto/manual-reset + // semantics of CreateEvent/SetEvent/ResetEvent. The Windows implementation never closes its + // handle, so this matches that one-time leak (ThreadSignals are long-lived) rather than adding + // a destructor to the shared header, which would also need a Windows definition. + namespace { + struct LinuxSignalState + { + std::mutex Mutex; + std::condition_variable Condition; + bool Signaled = false; + bool ManualReset = false; + }; + } + + ThreadSignal::ThreadSignal(const std::string& name, bool manualReset) + { + auto* state = new LinuxSignalState(); + state->ManualReset = manualReset; + m_SignalHandle = state; } - // TODO(Emily): `ThreadSignal` + void ThreadSignal::Wait() + { + auto* state = static_cast(m_SignalHandle); + std::unique_lock lock(state->Mutex); + state->Condition.wait(lock, [state] { return state->Signaled; }); + + // Auto-reset events consume the signal on a successful wait. + if (!state->ManualReset) + state->Signaled = false; + } + + void ThreadSignal::Signal() + { + auto* state = static_cast(m_SignalHandle); + { + std::lock_guard lock(state->Mutex); + state->Signaled = true; + } + // Manual-reset releases every waiter; auto-reset releases exactly one. + if (state->ManualReset) + state->Condition.notify_all(); + else + state->Condition.notify_one(); + } + + void ThreadSignal::Reset() + { + auto* state = static_cast(m_SignalHandle); + std::lock_guard lock(state->Mutex); + state->Signaled = false; + } std::thread::id Thread::GetID() const { diff --git a/Core/Source/Lux/Core/Assert.h b/Core/Source/Lux/Core/Assert.h index f74bfbc3..9fe6f4fc 100644 --- a/Core/Source/Lux/Core/Assert.h +++ b/Core/Source/Lux/Core/Assert.h @@ -7,6 +7,10 @@ #define LUX_DEBUG_BREAK __debugbreak() #elif defined(LUX_COMPILER_CLANG) #define LUX_DEBUG_BREAK __builtin_debugtrap() +#elif defined(LUX_COMPILER_GCC) && (defined(__i386__) || defined(__x86_64__)) +#define LUX_DEBUG_BREAK __asm__ volatile("int $0x03") +#elif defined(LUX_COMPILER_GCC) +#define LUX_DEBUG_BREAK __builtin_trap() #else #define LUX_DEBUG_BREAK #endif diff --git a/Core/Source/Lux/Core/Memory.h b/Core/Source/Lux/Core/Memory.h index 6fea87ac..b253b5a3 100644 --- a/Core/Source/Lux/Core/Memory.h +++ b/Core/Source/Lux/Core/Memory.h @@ -121,7 +121,8 @@ void __CRTDECL operator delete[](void* memory, const char* file, int line); #define ldelete delete #else -#warning "Memory tracking not available on non-Windows platform" +// Memory tracking relies on MSVC-specific global operator new/delete overloads, so it's a no-op +// on non-Windows platforms. lnew/ldelete fall back to plain new/delete (tracking simply disabled). #define lnew new #define ldelete delete diff --git a/Core/Source/Lux/Core/PlatformDetection.h b/Core/Source/Lux/Core/PlatformDetection.h index a7bd9652..49d8fecd 100644 --- a/Core/Source/Lux/Core/PlatformDetection.h +++ b/Core/Source/Lux/Core/PlatformDetection.h @@ -33,7 +33,6 @@ #error "Android is not supported!" #elif defined(__linux__) #define LUX_PLATFORM_LINUX -#error "Linux is not supported!" #else /* Unknown compiler/platform */ #error "Unknown platform!" diff --git a/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp index 7d6f105d..897780eb 100644 --- a/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp +++ b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp @@ -353,7 +353,11 @@ namespace Lux { m_AcknowledgedMacros.merge(includer->GetParsedSpecialMacros()); #else + // Linux resolves HLSL includes in the dxc CLI at compile time (via -I flags), so it + // skips the DXC-based preprocessor here. Still hash the raw source so the shader cache + // invalidates correctly when a shader changes. m_StagesMetadata[stage] = StageData{}; + m_StagesMetadata[stage].HashValue = Hash::GenerateFNVHash(shaderSource); #endif } return shaderSources; @@ -445,9 +449,9 @@ namespace Lux { return error; #elif defined(LUX_PLATFORM_LINUX) // Note(Emily): This is *atrocious* but dxc's integration refuses to process builtin HLSL without ICE'ing - // from the integration. + // from the integration. So we shell out to the dxc CLI instead. - char tempfileName[] = "hazel-hlsl-XXXXXX.spv"; + char tempfileName[] = "lux-hlsl-XXXXXX.spv"; int outfile = mkstemps(tempfileName, 4); std::string dxc = std::format("{}/bin/dxc", FileSystem::GetEnvironmentVariable("VULKAN_SDK")); @@ -476,7 +480,7 @@ namespace Lux { exec.push_back("-Zi"); } - if (stage & (VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT | VK_SHADER_STAGE_GEOMETRY_BIT)) + if ((uint16_t)stage & ((uint16_t)nvrhi::ShaderType::Vertex | (uint16_t)nvrhi::ShaderType::Hull | (uint16_t)nvrhi::ShaderType::Geometry)) exec.push_back("-fvk-invert-y"); exec.push_back(NULL); @@ -490,14 +494,14 @@ namespace Lux { char* env[] = { ld_lib_path.data(), NULL }; if (posix_spawn(&pid, exec[0], NULL, &attr, (char**)exec.data(), env)) { - return std::format("Could not execute `{}` for shader compilation: {} {}", exec[0], m_ShaderSourcePath.string(), ShaderUtils::ShaderStageToString(stage)); + return std::format("Could not execute `{}` for shader compilation: {} {}", exec[0], m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); } int status; waitpid(pid, &status, 0); if (WEXITSTATUS(status)) { - return std::format("Compilation failed\nWhile compiling shader file: {} \nAt stage: {}", m_ShaderSourcePath.string(), ShaderUtils::ShaderStageToString(stage)); + return std::format("Compilation failed\nWhile compiling shader file: {} \nAt stage: {}", m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); } off_t size = lseek(outfile, 0, SEEK_END); diff --git a/Core/Source/Lux/Scripting/ScriptBuilder.cpp b/Core/Source/Lux/Scripting/ScriptBuilder.cpp index 916195bf..3bcd40f4 100644 --- a/Core/Source/Lux/Scripting/ScriptBuilder.cpp +++ b/Core/Source/Lux/Scripting/ScriptBuilder.cpp @@ -13,13 +13,13 @@ namespace Lux { return false; } - // Route through PowerShell so quoting of the (possibly spaced) path is handled uniformly. - std::string quotedPath = "'" + projectPath.string() + "'"; + // Invoke dotnet directly. std::system routes through cmd.exe on Windows and /bin/sh on + // Linux; double-quoting the (possibly spaced) path is honoured by both, whereas a PowerShell + // wrapper is Windows-only. std::string command = - "powershell -NoProfile -ExecutionPolicy Bypass -Command \"& dotnet build " - + quotedPath + "dotnet build \"" + projectPath.string() + "\"" + " -c " + configuration - + " --nologo --verbosity minimal\""; + + " --nologo --verbosity minimal"; LUX_CORE_INFO("[ScriptBuilder] Building scripts ({})...", configuration); int result = std::system(command.c_str()); diff --git a/Core/premake5.lua b/Core/premake5.lua index e6484a2c..77aaf917 100644 --- a/Core/premake5.lua +++ b/Core/premake5.lua @@ -77,6 +77,11 @@ project "Core" defines { "LUX_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } links { "dw", "dl", "unwind", "pthread" } + -- HlslIncluder calls DxcCreateInstance (libdxcompiler), which Linux doesn't link: HLSL + -- include resolution happens in the dxc CLI at compile time, and the preprocessor no-op + -- on Linux never instantiates it. Drop it so the symbol isn't required at link time. + removefiles { "Source/Lux/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.cpp" } + filter "configurations:Debug or configurations:Debug-AS" symbols "On" defines { "LUX_DEBUG", "_DEBUG", "ACL_ON_ASSERT_ABORT", } diff --git a/Dependencies.lua b/Dependencies.lua index 2d89f6eb..6dbd1eb7 100644 --- a/Dependencies.lua +++ b/Dependencies.lua @@ -55,22 +55,24 @@ Dependencies = { }, }, DirectXCompiler = { - LibName = "dxcompiler", + -- Linux compiles HLSL by shelling out to the `dxc` binary (see VulkanShaderCompiler.cpp), + -- so libdxcompiler is only linked on Windows. + Windows = { LibName = "dxcompiler" }, }, TBB = { Linux = { LibName = "tbb" }, }, WinSock = { - LibName = "Ws2_32" + Windows = { LibName = "Ws2_32" }, }, WinMM = { - LibName = "Winmm" + Windows = { LibName = "Winmm" }, }, WinVersion = { - LibName = "Version" + Windows = { LibName = "Version" }, }, Bcrypt = { - LibName = "Bcrypt" + Windows = { LibName = "Bcrypt" }, }, -- Dropped entirely with "--no-aftermath"; the code side is gated on LUX_DISABLE_AFTERMATH. NvidiaAftermath = (not _OPTIONS["no-aftermath"]) and { @@ -148,18 +150,6 @@ Dependencies = { LibName = "NFD-Extended", IncludeDir = "%{wks.location}/Core/vendor/NFD-Extended/NFD-Extended/src/include" }, - ShaderC = { - LibName = "shaderc_shared", - Windows = { DebugLibName = "shaderc_sharedd", }, - IncludeDir = "%{wks.location}/Core/vendor/shaderc/include", - Configurations = "Debug,Release" - }, - ShaderCUtil = { - LibName = "shaderc_util", - Windows = { DebugLibName = "shaderc_utild", }, - IncludeDir = "%{wks.location}/Core/vendor/shaderc/libshaderc_util/include", - Configurations = "Debug,Release" - }, GLM = { IncludeDir = "%{wks.location}/Core/vendor/glm", }, @@ -167,7 +157,7 @@ Dependencies = { IncludeDir = "%{wks.location}/Core/vendor/entt/include", }, ImGuizmo = { - IncludeDir = "%{wks.location}/Core/vendor/ImGuizmo", + IncludeDir = "%{wks.location}/Core/vendor/imguizmo", }, STB = { IncludeDir = "%{wks.location}/Core/vendor/stb/include", @@ -178,7 +168,7 @@ Dependencies = { }, NVRHI = { LibName = "NVRHI", - IncludeDir = "%{wks.location}/Core/vendor/NVRHI/include" + IncludeDir = "%{wks.location}/Core/vendor/nvrhi/include" }, MiniAudio = { IncludeDir = "%{wks.location}/Core/vendor/miniaudio/include", @@ -206,9 +196,6 @@ Dependencies = { Freetype = { LibName = "freetype" }, - STB = { - IncludeDir = "%{wks.location}/Core/vendor/stb/include", - }, YAML_CPP = { IncludeDir = "%{wks.location}/Core/vendor/yaml-cpp/include", }, @@ -219,7 +206,7 @@ Dependencies = { Windows = { LibName = "ws2_32", }, }, Dbghelp = { - Windows = { LibName = " Dbghelp" }, + Windows = { LibName = "Dbghelp" }, }, } diff --git a/Editor/premake5.lua b/Editor/premake5.lua index 8c68ac28..31b852fe 100644 --- a/Editor/premake5.lua +++ b/Editor/premake5.lua @@ -61,8 +61,10 @@ project "Editor" defines { "LUX_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } links { "dw", "dl", "unwind", "pthread" } - result, err = os.outputof("pkg-config --libs gtk+-3.0") - linkoptions { result } + -- os.outputof runs at parse time on every host; pkg-config only exists on Linux. + if os.host() == "linux" then + linkoptions { os.outputof("pkg-config --libs gtk+-3.0") } + end filter "configurations:Debug or configurations:Debug-AS" symbols "On" diff --git a/premake5.lua b/premake5.lua index 04ff1db0..f7a7751f 100644 --- a/premake5.lua +++ b/premake5.lua @@ -134,7 +134,7 @@ group "Dependencies/Text" group "" group "Dependencies/Renderer" - include "Core/vendor/NVRHI" + include "Core/vendor/nvrhi" group "" group "Core" From c255366270fa8dd44a5f47d0378dbb2200b44767 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Wed, 22 Jul 2026 18:05:43 -0400 Subject: [PATCH 02/20] Add CLAUDE.md with build, architecture, and project docs Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..312cffa9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,139 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +LuxEngine is a C++20, Vulkan-only 3D game engine and editor. It is a solo project. The codebase compiles as a static library (`Core`), a C# scripting assembly (`ScriptCore`), an editor application (`Editor`), and a standalone runtime player (`Lux-Runtime`). Everything lives in the `Lux` C++ namespace. The active development branch for Linux support is `feature/linux`. + +--- + +## Build System + +Premake5 is used to generate build files. The binary is checked in at `./premake5`. + +**Generate Makefiles (Linux):** +```bash +./premake5 gmake2 +``` + +**Premake options:** +- `--no-tracy` — exclude Tracy profiler (useful to reduce link times) +- `--no-aftermath` — exclude Nvidia Aftermath GPU crash tracker +- `--discord` — enable Discord Social SDK integration (requires `Core/vendor/discord_social_sdk/`) + +**Build configs:** `debug`, `debug-as` (AddressSanitizer), `release`, `dist` + +**Build everything (debug):** +```bash +make config=debug +``` + +**Build a single project:** +```bash +make config=debug Editor +make config=debug Core +make config=debug Lux-Runtime +``` + +**Convenience group targets:** +```bash +make config=debug Dependencies # all third-party libs +make config=debug Core # Core + ScriptCore +make config=debug Tools # Editor +make config=debug Runtime # Lux-Runtime +``` + +**Clean:** +```bash +make clean +``` + +**ScriptCore** (the C# assembly) is also built via dotnet independently. The Coral post-build step copies `Coral.Managed.dll` into `Editor/DotNet/` automatically after a Core build. + +**Binaries** land in `bin/--x86_64//`. + +--- + +## Project Structure + +``` +Core/ + Source/Lux/ # Engine C++ source, organized by subsystem + Platform/ + Linux/ # Linux-specific: FileSystem, RenderThread, Thread + Windows/ # Windows-specific counterparts + Source/Lux/Platform/Vulkan/ # Vulkan backend + Source/lpch.h # Precompiled header (include via lpch.h) + vendor/ # All vendored C++ dependencies +ScriptCore/ + Source/Lux/ # C# scripting API (net9.0) + ScriptCore.csproj +Editor/ + Source/ # Editor application (ImGui panels, EditorLayer) +Lux-Runtime/ # Standalone runtime player +Dependencies.lua # Centralized dependency table (libs + include dirs) +premake5.lua # Workspace definition +``` + +--- + +## Architecture + +### Smart Pointers +- `Ref` — intrusive reference-counted pointer. Engine objects (meshes, textures, shaders, scenes, etc.) almost universally use `Ref`. Classes must inherit `RefCounted`. Use `Ref::Create(...)`. +- `Scope` — alias for `std::unique_ptr`, used for non-shared ownership. + +### ECS (Scene / Entity) +- `Scene` owns an `entt::registry`. `Entity` wraps an `entt::entity` + a `Scene*`. +- All component types are defined in `Core/Source/Lux/Scene/Components.h`. +- Scene serialization is YAML-based via `SceneSerializer`. +- Prefabs (`Prefab`) are serialized sub-hierarchies. + +### Renderer +- `SceneRenderer` is the main high-level renderer. It owns and drives the `RenderGraph`. +- `RenderGraph` manages passes, scratch-resource reuse, and compile caching. Passes are skipped (zero cost) when their feature is off. +- The pipeline is deferred PBR with a G-buffer, clustered (froxel) light culling, and a separate forward pass for transparents. +- The Vulkan backend lives in `Core/Source/Lux/Platform/Vulkan/`. All renderer API types (`Shader`, `Texture`, `Pipeline`, etc.) are abstract; their Vulkan implementations are in that folder. +- `Renderer2D` provides a 2D batch renderer (quads, circles, lines, MSDF text). +- Shader hot-reload and SPIR-V reflection caching are handled by `VulkanShaderCompiler` / `VulkanShaderCache`. +- On Linux, HLSL shaders are compiled by shelling out to `dxc`. `HlslIncluder.cpp` is excluded from Linux builds. + +### Asset Pipeline +- `AssetManager` is a static facade. Internally it delegates to `AssetManagerBase` (virtual interface). +- At runtime there are two concrete implementations: `EditorAssetManager` (editor, loads from source files) and `RuntimeAssetManager` (runtime, loads from binary asset packs). +- Every asset is identified by an `AssetHandle` (a `UUID`). Assets derive from `Asset`. +- Asset types are declared in `AssetTypes.h` and file extension mappings in `AssetExtensions.h`. + +### Scripting (C# / Coral) +- `ScriptEngine` manages the .NET 9 runtime via Coral (`Core/vendor/Coral/`). +- `ScriptGlue` registers C++ internal calls that `ScriptCore` calls via `[MethodImpl(MethodImplOptions.InternalCall)]`. +- `ScriptCore` (C# assembly) lives in `ScriptCore/Source/Lux/` and provides the public API to game scripts. +- Built assemblies are deployed to `Editor/Resources/Scripts/`. + +### Physics +- **3D**: Jolt Physics via `PhysicsSystem` / `PhysicsScene`. Jolt-specific wrappers in `Core/Source/Lux/Physics/JoltPhysics/`. +- **2D**: Box2D for 2D rigid bodies and colliders. +- Mesh colliders are cooked and cached by `MeshCookingFactory` / `MeshColliderCache`. + +### Audio +- miniaudio via `AudioEngine`, `AudioSource`, `AudioListener`. + +### Threading +- Optional dedicated render thread (`RenderThread`, platform-impl in `Core/Platform//`). +- Optional simulation thread (`SimulationThread`) — experimental, off by default. +- Job system in `Core/Source/Lux/Core/JobSystem`. + +### Profiling +- Tracy macros are wrapped in `Core/Source/Lux/Debug/Profiler.h` as `LUX_PROFILE_*`. +- Enabled by default in all configs except `dist` (or when `--no-tracy` is passed to premake). +- Nvidia Aftermath GPU crash dumps are in `Platform/Vulkan/Debug/` and excluded from `dist` builds. + +### Configuration Macros +- `LUX_PLATFORM_WINDOWS` / `LUX_PLATFORM_LINUX` +- `LUX_DEBUG` / `LUX_RELEASE` / `LUX_DIST` +- `LUX_TRACK_MEMORY` (debug + release only) +- `LUX_HAS_VULKAN` (always defined) + +### Adding a New Dependency +Edit `Dependencies.lua` — add an entry to the `Dependencies` table. Platform-specific lib names go in `Windows = { ... }` / `Linux = { ... }` sub-tables. The `ProcessDependencies()` / `IncludeDependencies()` helpers iterate it automatically; no manual `links {}` or `includedirs {}` needed in project files. From 15e7e5e3ab1681eb88397b856758e9fc30d6f905 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Wed, 22 Jul 2026 18:45:00 -0400 Subject: [PATCH 03/20] Update .gitignore for Linux and bump submodules for Linux compat Add ignore rules for generated Makefiles, vendored VulkanSDK, JoltPhysics build artifacts, premake5 binary, typescript, and .claude/ local settings. Update Coral, imgui, and nvrhi submodule refs to versions with better Linux support. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 24 ++++++++++++++++++++++++ Core/vendor/Coral | 2 +- Core/vendor/imgui | 2 +- Core/vendor/nvrhi | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 52760f07..ef31f253 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,30 @@ bin-int/ Intermediates/ /bin +# Generated Makefiles (Linux/gmake2) +Makefile +*.make +Core/Makefile +Editor/Makefile +Lux-Runtime/Makefile +ScriptCore/Makefile + +# Vendored Vulkan SDK (local install) +Core/vendor/VulkanSDK/ + +# JoltPhysics build artifacts +Core/vendor/JoltPhysics/Makefile +Core/vendor/JoltPhysics/bin/ + +# TypeScript (local) +typescript + +# Premake binary (platform-specific) +premake5 + +# Claude Code local settings +.claude/ + # Lux files *.log diff --git a/Core/vendor/Coral b/Core/vendor/Coral index d53b2685..e0299ca9 160000 --- a/Core/vendor/Coral +++ b/Core/vendor/Coral @@ -1 +1 @@ -Subproject commit d53b2685725f7535bc4d1deaa8a22bf16d112fe2 +Subproject commit e0299ca93b4609405149dee7fef64b7423273f0a diff --git a/Core/vendor/imgui b/Core/vendor/imgui index 7a0afd2d..86e47a45 160000 --- a/Core/vendor/imgui +++ b/Core/vendor/imgui @@ -1 +1 @@ -Subproject commit 7a0afd2ddb28dec1c85062c8fd472b58122dfe11 +Subproject commit 86e47a45085147969caa772f7b294edcc5f4f752 diff --git a/Core/vendor/nvrhi b/Core/vendor/nvrhi index 360ad30f..b5f74cc0 160000 --- a/Core/vendor/nvrhi +++ b/Core/vendor/nvrhi @@ -1 +1 @@ -Subproject commit 360ad30f53fbae3d9f4eff524f4cbe11105232fb +Subproject commit b5f74cc06f27812c13919953c06e18048501cdb5 From f32de214b47cc27ffa4b38d8ce3b616903fb879d Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Thu, 23 Jul 2026 00:27:05 -0400 Subject: [PATCH 04/20] Fix Linux/Wayland runtime: Vulkan semaphore ABI fix, build config selection, and remaining porting fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use C-style vkCreateSemaphore to avoid vulkan.hpp exception ABI issue with duplicate dispatch storage - Add VK_LAYER_PATH to Linux-Run.sh so validation layers are found from vendored SDK - Add interactive build config selection (debug/release/dist) to Linux-Build.sh and Linux-Run.sh - Fix Components.h → Scene.h includes for Clang template visibility (SceneHierarchyPanel, PhysicsScene2D) - Make .pdb postbuild copy Windows-only in Core premake - Add IMGUI_USE_WCHAR32 workspace define, --start-group linkoption, LinkNethost(), -lX11 - Fix os.outputof() multi-return in Editor premake, add NVRHI-Vulkan to Dependencies.lua - Rework Linux-Build.sh to bypass broken gmake ScriptCore target (use dotnet directly) Co-Authored-By: Claude Opus 4.6 --- Core/Source/Lux.h | 2 +- Core/Source/Lux/Audio/AudioSource.h | 2 +- Core/Source/Lux/Core/Application.h | 2 +- Core/Source/Lux/Core/Layer.h | 2 +- Core/Source/Lux/Core/Log.h | 10 ----- Core/Source/Lux/Editor/EditorCamera.h | 2 +- .../Source/Lux/Editor/SceneHierarchyPanel.cpp | 2 +- .../Lux/Physics/JoltPhysics/JoltBody.cpp | 2 +- .../JoltPhysics/JoltCharacterController.cpp | 2 +- .../Lux/Physics/JoltPhysics/JoltShapes.cpp | 2 +- Core/Source/Lux/Physics/PhysicsBody.cpp | 2 +- Core/Source/Lux/Physics/PhysicsScene.cpp | 2 +- Core/Source/Lux/Physics2D/PhysicsScene2D.cpp | 2 +- .../Lux/Platform/Vulkan/VulkanSwapChain.cpp | 27 +++++++----- Core/premake5.lua | 2 +- Core/vendor/nvrhi | 2 +- Dependencies.lua | 2 +- .../Source/Panels/ProjectSettingsWindow.cpp | 8 ++-- Editor/premake5.lua | 11 ++++- premake5.lua | 1 + scripts/Linux-Build.sh | 44 +++++++++++++++++-- scripts/Linux-Run.sh | 35 ++++++++++++++- 22 files changed, 119 insertions(+), 47 deletions(-) diff --git a/Core/Source/Lux.h b/Core/Source/Lux.h index a6f2f5af..893cfd80 100644 --- a/Core/Source/Lux.h +++ b/Core/Source/Lux.h @@ -7,7 +7,7 @@ #include "Lux/Core/Application.h" #include "Lux/Core/Log.h" #include "Lux/Core/Input.h" -#include "Lux/Core/TimeStep.h" +#include "Lux/Core/Timestep.h" #include "Lux/Core/Timer.h" //#include "Lux/Core/Platform.h" #include "Lux/Core/Version.h" diff --git a/Core/Source/Lux/Audio/AudioSource.h b/Core/Source/Lux/Audio/AudioSource.h index cbde6673..cf471be2 100644 --- a/Core/Source/Lux/Audio/AudioSource.h +++ b/Core/Source/Lux/Audio/AudioSource.h @@ -84,6 +84,6 @@ namespace Lux { std::filesystem::path m_FilePath; bool m_Spatialization = false; bool m_IsLoaded = false; - uint64_t m_CursorPos = 0; + ma_uint64 m_CursorPos = 0; }; } diff --git a/Core/Source/Lux/Core/Application.h b/Core/Source/Lux/Core/Application.h index 0d5a74f2..fa8db46a 100644 --- a/Core/Source/Lux/Core/Application.h +++ b/Core/Source/Lux/Core/Application.h @@ -1,7 +1,7 @@ #pragma once #include "Lux/Core/Base.h" -#include "Lux/Core/TimeStep.h" +#include "Lux/Core/Timestep.h" #include "Lux/Core/Timer.h" #include "Lux/Core/Window.h" #include "Lux/Core/LayerStack.h" diff --git a/Core/Source/Lux/Core/Layer.h b/Core/Source/Lux/Core/Layer.h index 165ce3fa..5b83f8f5 100644 --- a/Core/Source/Lux/Core/Layer.h +++ b/Core/Source/Lux/Core/Layer.h @@ -1,7 +1,7 @@ #pragma once #include "Lux/Core/Events/Event.h" -#include "Lux/Core/TimeStep.h" +#include "Lux/Core/Timestep.h" #include diff --git a/Core/Source/Lux/Core/Log.h b/Core/Source/Lux/Core/Log.h index 6c689e16..4bd96cf2 100644 --- a/Core/Source/Lux/Core/Log.h +++ b/Core/Source/Lux/Core/Log.h @@ -50,13 +50,8 @@ namespace Lux { static std::map& EnabledTags() { return s_EnabledTags; } static void SetDefaultTagSettings(); -#if defined(LUX_PLATFORM_WINDOWS) template static void PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args); -#else - template - static void PrintMessage(Log::Type type, Log::Level level, const std::string_view format, Args&&... args); -#endif template static void PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, std::format_string format, Args&&... args); @@ -146,13 +141,8 @@ namespace Lux { namespace Lux { -#if defined(LUX_PLATFORM_WINDOWS) template void Log::PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args) -#else - template - void Log::PrintMessage(Log::Type type, Log::Level level, const std::string_view format, Args&&... args) -#endif { auto detail = s_EnabledTags[""]; if (detail.Enabled && detail.LevelFilter <= level) diff --git a/Core/Source/Lux/Editor/EditorCamera.h b/Core/Source/Lux/Editor/EditorCamera.h index b11314c0..697666e7 100644 --- a/Core/Source/Lux/Editor/EditorCamera.h +++ b/Core/Source/Lux/Editor/EditorCamera.h @@ -3,7 +3,7 @@ #include #include "Lux/Renderer/Camera.h" -#include "Lux/Core/TimeStep.h" +#include "Lux/Core/Timestep.h" #include "Lux/Core/Events/KeyEvent.h" #include "Lux/Core/Events/MouseEvent.h" diff --git a/Core/Source/Lux/Editor/SceneHierarchyPanel.cpp b/Core/Source/Lux/Editor/SceneHierarchyPanel.cpp index 60a3dd62..24fc842d 100644 --- a/Core/Source/Lux/Editor/SceneHierarchyPanel.cpp +++ b/Core/Source/Lux/Editor/SceneHierarchyPanel.cpp @@ -14,7 +14,7 @@ #include "Lux/Renderer/Mesh.h" #include "Lux/Renderer/SceneEnvironment.h" #include "Lux/Renderer/UI/Font.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" #include "Lux/Scene/Prefab.h" #include "Lux/Scripting/ScriptEngine.h" #include "Lux/Core/Hash.h" diff --git a/Core/Source/Lux/Physics/JoltPhysics/JoltBody.cpp b/Core/Source/Lux/Physics/JoltPhysics/JoltBody.cpp index 42164127..e7b8579a 100644 --- a/Core/Source/Lux/Physics/JoltPhysics/JoltBody.cpp +++ b/Core/Source/Lux/Physics/JoltPhysics/JoltBody.cpp @@ -3,7 +3,7 @@ #include "JoltUtils.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" #include #include diff --git a/Core/Source/Lux/Physics/JoltPhysics/JoltCharacterController.cpp b/Core/Source/Lux/Physics/JoltPhysics/JoltCharacterController.cpp index bfe580e3..c6bb0738 100644 --- a/Core/Source/Lux/Physics/JoltPhysics/JoltCharacterController.cpp +++ b/Core/Source/Lux/Physics/JoltPhysics/JoltCharacterController.cpp @@ -1,7 +1,7 @@ #include "lpch.h" #include "JoltCharacterController.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" #include diff --git a/Core/Source/Lux/Physics/JoltPhysics/JoltShapes.cpp b/Core/Source/Lux/Physics/JoltPhysics/JoltShapes.cpp index 88d096ab..10f366fe 100644 --- a/Core/Source/Lux/Physics/JoltPhysics/JoltShapes.cpp +++ b/Core/Source/Lux/Physics/JoltPhysics/JoltShapes.cpp @@ -4,7 +4,7 @@ #include "JoltUtils.h" #include "Lux/Asset/AssetManager.h" #include "Lux/Renderer/Mesh.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" #include "Lux/Scene/Scene.h" #include diff --git a/Core/Source/Lux/Physics/PhysicsBody.cpp b/Core/Source/Lux/Physics/PhysicsBody.cpp index 0f41f7d0..e299f79c 100644 --- a/Core/Source/Lux/Physics/PhysicsBody.cpp +++ b/Core/Source/Lux/Physics/PhysicsBody.cpp @@ -1,7 +1,7 @@ #include "lpch.h" #include "PhysicsBody.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" namespace Lux { diff --git a/Core/Source/Lux/Physics/PhysicsScene.cpp b/Core/Source/Lux/Physics/PhysicsScene.cpp index 0a4dc687..1a6535a0 100644 --- a/Core/Source/Lux/Physics/PhysicsScene.cpp +++ b/Core/Source/Lux/Physics/PhysicsScene.cpp @@ -10,7 +10,7 @@ #include "Lux/Physics/PhysicsBody.h" #include "Lux/Project/Project.h" #include "Lux/Renderer/Mesh.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" #include "Lux/Scene/Entity.h" #include "Lux/Scene/Scene.h" diff --git a/Core/Source/Lux/Physics2D/PhysicsScene2D.cpp b/Core/Source/Lux/Physics2D/PhysicsScene2D.cpp index 134d407d..0e45ec8a 100644 --- a/Core/Source/Lux/Physics2D/PhysicsScene2D.cpp +++ b/Core/Source/Lux/Physics2D/PhysicsScene2D.cpp @@ -3,7 +3,7 @@ #include "ContactListener2D.h" -#include "Lux/Scene/Components.h" +#include "Lux/Scene/Scene.h" #include "Lux/Scene/Entity.h" #include "Lux/Scene/Scene.h" diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp index 6a7c72f5..fc243aaf 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp @@ -128,17 +128,22 @@ namespace Lux { m_SwapChainIndex = 0; // Create acquire semaphores (for frame synchronization) - for (uint32_t i = 0; i < 3; ++i) { - m_AcquireSemaphores[i] = vulkanDeviceManager->m_VulkanDevice.createSemaphore(vk::SemaphoreCreateInfo()); - } + vk::SemaphoreCreateInfo semCI; + for (uint32_t i = 0; i < 3; ++i) + { + const vk::Result semRes = vulkanDeviceManager->m_VulkanDevice.createSemaphore(&semCI, nullptr, &m_AcquireSemaphores[i]); + LUX_CORE_VERIFY(semRes == vk::Result::eSuccess); + } - // Create one present semaphore per swapchain image to avoid reuse conflicts - // (see: https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html) - m_PresentSemaphores.resize(m_SwapChainImages.size()); - for (size_t i = 0; i < m_SwapChainImages.size(); ++i) - { - m_PresentSemaphores[i] = vulkanDeviceManager->m_VulkanDevice.createSemaphore(vk::SemaphoreCreateInfo()); + // Create one present semaphore per swapchain image to avoid reuse conflicts + // (see: https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html) + m_PresentSemaphores.resize(m_SwapChainImages.size()); + for (size_t i = 0; i < m_SwapChainImages.size(); ++i) + { + const vk::Result semRes = vulkanDeviceManager->m_VulkanDevice.createSemaphore(&semCI, nullptr, &m_PresentSemaphores[i]); + LUX_CORE_VERIFY(semRes == vk::Result::eSuccess); + } } BackBufferResized(); @@ -273,9 +278,9 @@ namespace Lux { RenderCommandBuffer::UnlockQueue(); #ifndef _WIN32 - if (deviceParams.vsyncEnabled) + if (vulkanDeviceManager->m_DeviceParams.vsyncEnabled) { - m_PresentQueue.waitIdle(); + vulkanDeviceManager->m_PresentQueue.waitIdle(); } #endif diff --git a/Core/premake5.lua b/Core/premake5.lua index 77aaf917..ed81594c 100644 --- a/Core/premake5.lua +++ b/Core/premake5.lua @@ -14,7 +14,7 @@ project "Core" '{COPYFILE} "%{wks.location}/Core/vendor/Coral/Build/%{cfg.buildcfg}/Coral.Managed.deps.json" "%{wks.location}/Editor/DotNet/Coral.Managed.deps.json"', } - filter { "configurations:Debug or configurations:Debug-AS or configurations:Release" } + filter { "system:windows", "configurations:Debug or configurations:Debug-AS or configurations:Release" } postbuildcommands { '{COPYFILE} "%{wks.location}/Core/vendor/Coral/Build/%{cfg.buildcfg}/Coral.Managed.pdb" "%{wks.location}/Editor/DotNet/Coral.Managed.pdb"', } diff --git a/Core/vendor/nvrhi b/Core/vendor/nvrhi index b5f74cc0..360ad30f 160000 --- a/Core/vendor/nvrhi +++ b/Core/vendor/nvrhi @@ -1 +1 @@ -Subproject commit b5f74cc06f27812c13919953c06e18048501cdb5 +Subproject commit 360ad30f53fbae3d9f4eff524f4cbe11105232fb diff --git a/Dependencies.lua b/Dependencies.lua index 6dbd1eb7..955e485c 100644 --- a/Dependencies.lua +++ b/Dependencies.lua @@ -167,7 +167,7 @@ Dependencies = { IncludeDir = "%{wks.location}/Core/vendor/imgui", }, NVRHI = { - LibName = "NVRHI", + LibName = { "NVRHI", "NVRHI-Vulkan" }, IncludeDir = "%{wks.location}/Core/vendor/nvrhi/include" }, MiniAudio = { diff --git a/Editor/Source/Panels/ProjectSettingsWindow.cpp b/Editor/Source/Panels/ProjectSettingsWindow.cpp index f35e96c8..bdfa8614 100644 --- a/Editor/Source/Panels/ProjectSettingsWindow.cpp +++ b/Editor/Source/Panels/ProjectSettingsWindow.cpp @@ -362,10 +362,10 @@ namespace Lux { const std::string scriptModulePath = m_Project->GetConfig().ScriptModulePath.generic_string(); const std::string& defaultNamespace = m_Project->GetConfig().DefaultNamespace; - strncpy_s(m_NameBuffer, name.c_str(), _TRUNCATE); - strncpy_s(m_RuntimeGameNameBuffer, runtimeGameName.c_str(), _TRUNCATE); - strncpy_s(m_ScriptModulePathBuffer, scriptModulePath.c_str(), _TRUNCATE); - strncpy_s(m_DefaultNamespaceBuffer, defaultNamespace.c_str(), _TRUNCATE); + std::strncpy(m_NameBuffer, name.c_str(), sizeof(m_NameBuffer) - 1); + std::strncpy(m_RuntimeGameNameBuffer, runtimeGameName.c_str(), sizeof(m_RuntimeGameNameBuffer) - 1); + std::strncpy(m_ScriptModulePathBuffer, scriptModulePath.c_str(), sizeof(m_ScriptModulePathBuffer) - 1); + std::strncpy(m_DefaultNamespaceBuffer, defaultNamespace.c_str(), sizeof(m_DefaultNamespaceBuffer) - 1); } void ProjectSettingsWindow::SaveProject() diff --git a/Editor/premake5.lua b/Editor/premake5.lua index 31b852fe..239ed942 100644 --- a/Editor/premake5.lua +++ b/Editor/premake5.lua @@ -59,11 +59,18 @@ project "Editor" filter "system:linux" defines { "LUX_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } - links { "dw", "dl", "unwind", "pthread" } + links { "dw", "dl", "unwind", "pthread", "X11" } + linkoptions { "-Wl,--start-group" } + + -- Link nethost for Coral .NET hosting + if os.host() == "linux" then + LinkNethost() + end -- os.outputof runs at parse time on every host; pkg-config only exists on Linux. if os.host() == "linux" then - linkoptions { os.outputof("pkg-config --libs gtk+-3.0") } + local gtklibs, _ = os.outputof("pkg-config --libs gtk+-3.0") + linkoptions { gtklibs } end filter "configurations:Debug or configurations:Debug-AS" diff --git a/premake5.lua b/premake5.lua index f7a7751f..84278573 100644 --- a/premake5.lua +++ b/premake5.lua @@ -42,6 +42,7 @@ workspace "Lux" "LUX_HAS_VULKAN", "VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1", "IMGUI_DEFINE_MATH_OPERATORS", + "IMGUI_USE_WCHAR32", "YAML_CPP_STATIC_DEFINE", } diff --git a/scripts/Linux-Build.sh b/scripts/Linux-Build.sh index 44760f36..c838b02d 100644 --- a/scripts/Linux-Build.sh +++ b/scripts/Linux-Build.sh @@ -12,8 +12,35 @@ fi if [ -n "${BUILD_CONFIG+set}" ] then true + elif [ -n "$1" ] + then + case "$1" in + debug|Debug) export BUILD_CONFIG=Debug ;; + release|Release) export BUILD_CONFIG=Release ;; + dist|Dist) export BUILD_CONFIG=Dist ;; + *) + echo "Unknown config: $1" + echo "Usage: $0 [debug|release|dist]" + exit 1 + ;; + esac + shift else - export BUILD_CONFIG=Debug + echo "Select build configuration:" + echo " 1) Debug" + echo " 2) Release" + echo " 3) Dist" + printf "Choice [1-3]: " + read choice + case "$choice" in + 1) export BUILD_CONFIG=Debug ;; + 2) export BUILD_CONFIG=Release ;; + 3) export BUILD_CONFIG=Dist ;; + *) + echo "Invalid choice" + exit 1 + ;; + esac fi if [ -n "${VULKAN_SDK+set}" ] @@ -30,6 +57,15 @@ fi Core/vendor/Coral/Coral.Managed/Coral.Managed-Static.csproj -o Editor/DotNet dotnet build -c $BUILD_CONFIG --property WarningLevel=0 ScriptCore/ScriptCore.csproj -# Build Lux - premake5 gmake --cc=clang --verbose - make config=$(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') "$@" +# Ensure Coral build artifacts exist where the Core postbuild step expects them + mkdir -p Core/vendor/Coral/Build/Release + cp -f Editor/DotNet/Coral.Managed.dll Core/vendor/Coral/Build/Release/ + cp -f Editor/DotNet/Coral.Managed.runtimeconfig.json Core/vendor/Coral/Build/Release/ 2>/dev/null || true + cp -f Editor/DotNet/Coral.Managed.deps.json Core/vendor/Coral/Build/Release/ 2>/dev/null || true + +# Build Lux (skip the gmake ScriptCore target — it was already built above via dotnet) + ./premake5 gmake2 --cc=clang + CONFIG=$(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') + make config=$CONFIG Dependencies Dependencies/Renderer "$@" + make -C Core -f Makefile config=$CONFIG + make -C Editor -f Makefile config=$CONFIG diff --git a/scripts/Linux-Run.sh b/scripts/Linux-Run.sh index c60b4335..8699d8ca 100644 --- a/scripts/Linux-Run.sh +++ b/scripts/Linux-Run.sh @@ -1,8 +1,41 @@ #!/bin/sh export LUX_DIR=$(realpath .) -export BUILD_CONFIG=${BUILD_CONFIG:-Debug} +if [ -n "${BUILD_CONFIG+set}" ] + then + true + elif [ -n "$1" ] + then + case "$1" in + debug|Debug) export BUILD_CONFIG=Debug ;; + release|Release) export BUILD_CONFIG=Release ;; + dist|Dist) export BUILD_CONFIG=Dist ;; + *) + echo "Unknown config: $1" + echo "Usage: $0 [debug|release|dist]" + exit 1 + ;; + esac + shift + else + echo "Select build configuration:" + echo " 1) Debug" + echo " 2) Release" + echo " 3) Dist" + printf "Choice [1-3]: " + read choice + case "$choice" in + 1) export BUILD_CONFIG=Debug ;; + 2) export BUILD_CONFIG=Release ;; + 3) export BUILD_CONFIG=Dist ;; + *) + echo "Invalid choice" + exit 1 + ;; + esac +fi export VULKAN_SDK=$(realpath Core/vendor/VulkanSDK/x86_64) +export VK_LAYER_PATH="$VULKAN_SDK/share/vulkan/explicit_layer.d" export LD_LIBRARY_PATH="$VULKAN_SDK/lib:$LUX_DIR/Core/vendor/assimp/bin/linux:$LUX_DIR/Core/vendor/NvidiaAftermath/lib/x64/linux" cd Editor From 2312699dbe1b5ae2316e6ec1831ef9f32c8fa636 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Thu, 23 Jul 2026 01:11:39 -0400 Subject: [PATCH 05/20] Fix Vulkan swapchain UB, shader compilation, and Linux runtime environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing `return true` in VulkanSwapChain::Create() — undefined behavior from falling off a non-void function caused heap corruption on Linux - Fix Linux HLSL shader compilation: write stage-specific source to temp file instead of passing the entire multi-stage .hlsl to dxc (which caused struct redefinitions across stages) - Capture dxc stderr via pipe()/posix_spawn so shader errors are reported - Use C-style getSwapchainImagesKHR to match the semaphore fix - Add VK_LAYER_PATH and $VULKAN_SDK/bin to PATH in Linux-Run.sh Co-Authored-By: Claude Opus 4.6 --- .../ShaderCompiler/VulkanShaderCompiler.cpp | 60 ++++++++++++++----- .../Lux/Platform/Vulkan/VulkanSwapChain.cpp | 7 ++- scripts/Linux-Run.sh | 1 + 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp index 897780eb..97730567 100644 --- a/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp +++ b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp @@ -28,6 +28,7 @@ #include #if defined(LUX_PLATFORM_LINUX) +#include #include #include #include @@ -448,18 +449,22 @@ namespace Lux { return error; #elif defined(LUX_PLATFORM_LINUX) - // Note(Emily): This is *atrocious* but dxc's integration refuses to process builtin HLSL without ICE'ing - // from the integration. So we shell out to the dxc CLI instead. + // Write stage-specific source to a temp file so dxc only sees one stage + // (the preprocessor already split the multi-stage .hlsl by #pragma stage). + char srcTempName[] = "/tmp/lux-hlsl-src-XXXXXX.hlsl"; + int srcFd = mkstemps(srcTempName, 5); + write(srcFd, stageSource.c_str(), stageSource.size()); + close(srcFd); - char tempfileName[] = "lux-hlsl-XXXXXX.spv"; - int outfile = mkstemps(tempfileName, 4); + char outTempName[] = "/tmp/lux-hlsl-out-XXXXXX.spv"; + int outFd = mkstemps(outTempName, 4); + close(outFd); std::string dxc = std::format("{}/bin/dxc", FileSystem::GetEnvironmentVariable("VULKAN_SDK")); - std::string sourcePath = m_ShaderSourcePath.string(); std::vector exec{ dxc.c_str(), - sourcePath.c_str(), + srcTempName, "-E", "main", "-T", ShaderUtils::HLSLShaderProfile(stage), @@ -471,7 +476,7 @@ namespace Lux { "-I", "Resources/Shaders/Include/Common", "-I", "Resources/Shaders/Include/HLSL", - "-Fo", tempfileName + "-Fo", outTempName }; if (options.GenerateDebugInfo) @@ -485,31 +490,56 @@ namespace Lux { exec.push_back(NULL); - // TODO(Emily): Error handling + // Capture stderr from dxc for error messages + int errPipe[2]; + pipe(errPipe); + + posix_spawn_file_actions_t fileActions; + posix_spawn_file_actions_init(&fileActions); + posix_spawn_file_actions_adddup2(&fileActions, errPipe[1], STDERR_FILENO); + posix_spawn_file_actions_addclose(&fileActions, errPipe[0]); + pid_t pid; - posix_spawnattr_t attr; - posix_spawnattr_init(&attr); + std::string ld_lib_path = std::format("LD_LIBRARY_PATH={}", getenv("LD_LIBRARY_PATH") ? getenv("LD_LIBRARY_PATH") : ""); + char* spawnEnv[] = { ld_lib_path.data(), NULL }; + int spawnErr = posix_spawn(&pid, exec[0], &fileActions, nullptr, (char**)exec.data(), spawnEnv); + close(errPipe[1]); - std::string ld_lib_path = std::format("LD_LIBRARY_PATH={}", getenv("LD_LIBRARY_PATH")); - char* env[] = { ld_lib_path.data(), NULL }; - if (posix_spawn(&pid, exec[0], NULL, &attr, (char**)exec.data(), env)) + if (spawnErr) { + close(errPipe[0]); + unlink(srcTempName); + unlink(outTempName); return std::format("Could not execute `{}` for shader compilation: {} {}", exec[0], m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); } + + // Read dxc stderr + std::string dxcErrors; + char buf[4096]; + ssize_t n; + while ((n = read(errPipe[0], buf, sizeof(buf))) > 0) + dxcErrors.append(buf, n); + close(errPipe[0]); + int status; waitpid(pid, &status, 0); + posix_spawn_file_actions_destroy(&fileActions); + + unlink(srcTempName); if (WEXITSTATUS(status)) { - return std::format("Compilation failed\nWhile compiling shader file: {} \nAt stage: {}", m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); + unlink(outTempName); + return std::format("{}\nWhile compiling shader file: {} \nAt stage: {}", dxcErrors, m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); } + int outfile = open(outTempName, O_RDONLY); off_t size = lseek(outfile, 0, SEEK_END); lseek(outfile, 0, SEEK_SET); outputBinary.resize(size / sizeof(uint32_t)); read(outfile, outputBinary.data(), size); close(outfile); - unlink(tempfileName); + unlink(outTempName); return {}; #endif diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp index fc243aaf..88f89c12 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp @@ -104,9 +104,11 @@ namespace Lux { LUX_CORE_ERROR("Failed to create a Vulkan swap chain, error code = {}", nvrhi::vulkan::resultToString(VkResult(res))); return false; } - // retrieve swap chain images - auto images = vulkanDeviceManager->m_VulkanDevice.getSwapchainImagesKHR(m_SwapChain); + uint32_t imageCount = 0; + vulkanDeviceManager->m_VulkanDevice.getSwapchainImagesKHR(m_SwapChain, &imageCount, nullptr); + std::vector images(imageCount); + vulkanDeviceManager->m_VulkanDevice.getSwapchainImagesKHR(m_SwapChain, &imageCount, images.data()); for (auto image : images) { SwapChainImage sci; @@ -147,6 +149,7 @@ namespace Lux { } BackBufferResized(); + return true; } void VulkanSwapChain::Destroy() diff --git a/scripts/Linux-Run.sh b/scripts/Linux-Run.sh index 8699d8ca..9462d3b8 100644 --- a/scripts/Linux-Run.sh +++ b/scripts/Linux-Run.sh @@ -36,6 +36,7 @@ if [ -n "${BUILD_CONFIG+set}" ] fi export VULKAN_SDK=$(realpath Core/vendor/VulkanSDK/x86_64) export VK_LAYER_PATH="$VULKAN_SDK/share/vulkan/explicit_layer.d" +export PATH="$VULKAN_SDK/bin:$PATH" export LD_LIBRARY_PATH="$VULKAN_SDK/lib:$LUX_DIR/Core/vendor/assimp/bin/linux:$LUX_DIR/Core/vendor/NvidiaAftermath/lib/x64/linux" cd Editor From c6367a94dc6a733fe98c2fa0404c0cb7ec823e3a Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Thu, 23 Jul 2026 01:57:59 -0400 Subject: [PATCH 06/20] Fix debug build crash and improve Linux/Wayland editor behavior - Fix Log::PrintMessage map corruption: replace operator[] with find() to avoid mutating s_EnabledTags (not thread-safe, crashed in debug builds via _Rb_tree_decrement). Add empty-string key to default tag settings. - Make Vulkan validation layer optional in debug builds so the editor starts even without VK_LAYER_KHRONOS_validation installed. - Use glfwDragWindow() for titlebar dragging on Linux instead of glfwSetWindowPos() which is a no-op on Wayland. Co-Authored-By: Claude Opus 4.6 --- Core/Source/Lux/Core/Log.cpp | 1 + Core/Source/Lux/Core/Log.h | 15 ++++++++++++--- .../Lux/Platform/Vulkan/VulkanDeviceManager.cpp | 2 +- Editor/Source/EditorLayer.cpp | 5 +---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Core/Source/Lux/Core/Log.cpp b/Core/Source/Lux/Core/Log.cpp index 5fbfe92b..803db5ca 100644 --- a/Core/Source/Lux/Core/Log.cpp +++ b/Core/Source/Lux/Core/Log.cpp @@ -16,6 +16,7 @@ namespace Lux { std::shared_ptr Log::s_EditorConsoleLogger; std::map Log::s_DefaultTagDetails = { + { "", TagDetails{ true, Level::Trace } }, { "Animation", TagDetails{ true, Level::Warn } }, { "Asset Pack", TagDetails{ true, Level::Warn } }, { "AssetManager", TagDetails{ true, Level::Info } }, diff --git a/Core/Source/Lux/Core/Log.h b/Core/Source/Lux/Core/Log.h index 4bd96cf2..1e3785b0 100644 --- a/Core/Source/Lux/Core/Log.h +++ b/Core/Source/Lux/Core/Log.h @@ -144,7 +144,10 @@ namespace Lux { template void Log::PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args) { - auto detail = s_EnabledTags[""]; + auto it = s_EnabledTags.find(""); + if (it == s_EnabledTags.end()) + return; + const auto& detail = it->second; if (detail.Enabled && detail.LevelFilter <= level) { auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); @@ -173,7 +176,10 @@ namespace Lux { template void Log::PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, const std::format_string format, Args&&... args) { - auto detail = s_EnabledTags[std::string(tag)]; + auto it = s_EnabledTags.find(std::string(tag)); + if (it == s_EnabledTags.end()) + return; + const auto& detail = it->second; if (detail.Enabled && detail.LevelFilter <= level) { auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); @@ -202,7 +208,10 @@ namespace Lux { inline void Log::PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, std::string_view message) { - auto detail = s_EnabledTags[std::string(tag)]; + auto it = s_EnabledTags.find(std::string(tag)); + if (it == s_EnabledTags.end()) + return; + const auto& detail = it->second; if (detail.Enabled && detail.LevelFilter <= level) { auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanDeviceManager.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanDeviceManager.cpp index f095d901..226bb49a 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanDeviceManager.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanDeviceManager.cpp @@ -612,7 +612,7 @@ namespace Lux { if (m_DeviceParams.enableDebugRuntime) { enabledExtensions.instance.insert("VK_EXT_debug_report"); - enabledExtensions.layers.insert("VK_LAYER_KHRONOS_validation"); + optionalExtensions.layers.insert("VK_LAYER_KHRONOS_validation"); } PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = diff --git a/Editor/Source/EditorLayer.cpp b/Editor/Source/EditorLayer.cpp index 4d0884d7..20b58209 100644 --- a/Editor/Source/EditorLayer.cpp +++ b/Editor/Source/EditorLayer.cpp @@ -1341,10 +1341,7 @@ namespace Lux { #if !defined(LUX_PLATFORM_WINDOWS) if (nativeWindow && !isMaximized && ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - int windowX = 0, windowY = 0; - glfwGetWindowPos(nativeWindow, &windowX, &windowY); - const ImVec2 delta = ImGui::GetIO().MouseDelta; - glfwSetWindowPos(nativeWindow, windowX + (int)delta.x, windowY + (int)delta.y); + glfwDragWindow(nativeWindow); } #endif From b9b94e08b1ebcc063c89dc55a50e5b0ee9a6eb77 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Thu, 23 Jul 2026 01:59:44 -0400 Subject: [PATCH 07/20] Add null checks for ImGui texture rendering to prevent crashes - Guard GetTextureID() against null Image2D and Texture2D refs - Guard DrawButtonImage() against null texture parameters - Prevents crashes when editor icon textures fail to load Co-Authored-By: Claude Opus 4.6 --- Core/Source/Lux/ImGui/ImGuiUtilities.cpp | 12 ++++++++---- Core/Source/Lux/ImGui/VulkanImGui.cpp | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Core/Source/Lux/ImGui/ImGuiUtilities.cpp b/Core/Source/Lux/ImGui/ImGuiUtilities.cpp index a237319b..c3cd01f0 100644 --- a/Core/Source/Lux/ImGui/ImGuiUtilities.cpp +++ b/Core/Source/Lux/ImGui/ImGuiUtilities.cpp @@ -703,11 +703,13 @@ namespace Lux::ImGuiEx { { if (rectMin.x > rectMax.x || rectMin.y > rectMax.y) return; + if (!imageNormal) + return; auto* drawList = ImGui::GetWindowDrawList(); - if (ImGui::IsItemActive()) + if (ImGui::IsItemActive() && imagePressed) drawList->AddImage(GetTextureID(imagePressed), rectMin, rectMax, uv0, uv1, tintPressed); - else if (ImGui::IsItemHovered()) + else if (ImGui::IsItemHovered() && imageHovered) drawList->AddImage(GetTextureID(imageHovered), rectMin, rectMax, uv0, uv1, tintHovered); else drawList->AddImage(GetTextureID(imageNormal), rectMin, rectMax, uv0, uv1, tintNormal); @@ -719,11 +721,13 @@ namespace Lux::ImGuiEx { { if (rectMin.x > rectMax.x || rectMin.y > rectMax.y) return; + if (!imageNormal) + return; auto* drawList = ImGui::GetWindowDrawList(); - if (ImGui::IsItemActive()) + if (ImGui::IsItemActive() && imagePressed) drawList->AddImage(GetTextureID(imagePressed), rectMin, rectMax, uv0, uv1, tintPressed); - else if (ImGui::IsItemHovered()) + else if (ImGui::IsItemHovered() && imageHovered) drawList->AddImage(GetTextureID(imageHovered), rectMin, rectMax, uv0, uv1, tintHovered); else drawList->AddImage(GetTextureID(imageNormal), rectMin, rectMax, uv0, uv1, tintNormal); diff --git a/Core/Source/Lux/ImGui/VulkanImGui.cpp b/Core/Source/Lux/ImGui/VulkanImGui.cpp index 2f551ef3..6d820d36 100644 --- a/Core/Source/Lux/ImGui/VulkanImGui.cpp +++ b/Core/Source/Lux/ImGui/VulkanImGui.cpp @@ -16,6 +16,8 @@ namespace Lux::ImGuiEx { ImTextureID GetTextureID(Ref image, ImageMode mode) { + if (!image) + return (ImTextureID)0; return Application::Get().GetImGuiLayer()->GetImGuiRenderer()->CreateFrameTexture( image->GetHandle().Get(), nvrhi::AllSubresources, (mode == ImageMode::Opaque), (mode == ImageMode::Depth)); } @@ -36,6 +38,8 @@ namespace Lux::ImGuiEx { ImTextureID GetTextureID(Ref texture) { + if (!texture || !texture->GetImage()) + return (ImTextureID)0; return GetTextureID(texture->GetImage(), ImageMode::Normal); } From 6c04715c23fe7b43157ac00378030e8619551f83 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Mon, 27 Jul 2026 19:19:34 -0400 Subject: [PATCH 08/20] Fix HLSL shader macros, Wayland camera, and build system for Linux Pass global macros (-D flags) to dxc CLI on Linux so HLSL shaders compile with the same defines as GLSL (fixes black screen at Ultra/Cinematic quality). Include global macros in HLSL hash for correct shader cache invalidation. Use CursorMode::Locked on Linux to avoid erratic camera when the mouse exits the window (glfwSetCursorPos is a no-op on Wayland). Move nvrhi build adaptations (DefaultTargetParams, HazelRootDirectory, NVRHI_WITH_RTXMU, Vulkan include paths) from submodule to workspace premake5.lua. Add VULKAN_SDK fallback to repo-bundled SDK in Dependencies.lua. Add Tracy forceincludes fix for GCC on Linux. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_018SYjJnCeBqSsQCxSguSKSC --- Core/Source/Lux/Core/Application.cpp | 12 ++-- Core/Source/Lux/Core/Timer.h | 1 + Core/Source/Lux/Core/Window.cpp | 24 ++++---- Core/Source/Lux/Core/Window.h | 3 +- Core/Source/Lux/Editor/EditorCamera.cpp | 4 ++ Core/Source/Lux/ImGui/ImGuiLayer.cpp | 2 + .../ShaderCompiler/VulkanShaderCompiler.cpp | 29 ++++++++-- .../Lux/Platform/Vulkan/VulkanSwapChain.cpp | 29 ++++++++-- Core/Source/Lux/Renderer/Renderer.cpp | 2 + .../Lux/Scripting/ScriptEntityStorage.cpp | 1 + Core/vendor/nvrhi | 2 +- Dependencies.lua | 5 +- Editor/LuxSampleProject/LuxSample.luxproj | 2 +- Editor/Source/EditorLayer.cpp | 55 +++++++++++-------- Editor/Source/LuxEditorApp.cpp | 6 ++ premake5.lua | 47 ++++++++++++++++ scripts/Linux-Run.sh | 0 .../compat/Hazel-ScriptCore/Source/Dummy.cpp | 1 + 18 files changed, 172 insertions(+), 53 deletions(-) mode change 100644 => 100755 scripts/Linux-Run.sh create mode 100644 scripts/compat/Hazel-ScriptCore/Source/Dummy.cpp diff --git a/Core/Source/Lux/Core/Application.cpp b/Core/Source/Lux/Core/Application.cpp index f70ecbc5..d35e68ba 100644 --- a/Core/Source/Lux/Core/Application.cpp +++ b/Core/Source/Lux/Core/Application.cpp @@ -242,10 +242,11 @@ namespace Lux { Timer cpuTimer; // On Render thread + bool frameBeginSuccess = true; Renderer::Submit([&]() { - //m_Window->GetSwapChain().BeginFrame(); - m_Window->BeginFrame(); + if (!m_Window->BeginFrame()) + frameBeginSuccess = false; }); Renderer::BeginFrame(); @@ -282,9 +283,10 @@ namespace Lux { // On Render thread Renderer::Submit([&]() { - // m_Window->GetSwapChain().BeginFrame(); - // Renderer::WaitAndRender(); - m_Window->Present(); + if (frameBeginSuccess) + { + m_Window->Present(); + } GetGraphicsDevice()->runGarbageCollection(); }); diff --git a/Core/Source/Lux/Core/Timer.h b/Core/Source/Lux/Core/Timer.h index 01f2f40a..4a3ec43a 100644 --- a/Core/Source/Lux/Core/Timer.h +++ b/Core/Source/Lux/Core/Timer.h @@ -49,6 +49,7 @@ namespace Lux { inline PerFrameData& operator+=(float time) { Time += time; + return *this; } }; public: diff --git a/Core/Source/Lux/Core/Window.cpp b/Core/Source/Lux/Core/Window.cpp index 04b5bf90..9d32b82e 100644 --- a/Core/Source/Lux/Core/Window.cpp +++ b/Core/Source/Lux/Core/Window.cpp @@ -328,6 +328,13 @@ namespace Lux { data.EventCallback(event); data.Width = width; data.Height = height; + data.SizeDirty = true; + }); + + glfwSetFramebufferSizeCallback(m_WindowHandle, [](GLFWwindow* window, int, int) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + data.SizeDirty = true; }); glfwSetWindowCloseCallback(m_WindowHandle, [](GLFWwindow* window) @@ -506,17 +513,10 @@ namespace Lux { glfwPollEvents(); Input::Update(); - // m_DeviceManager->UpdateWindowSize(); - int width; - int height; - glfwGetWindowSize(m_WindowHandle, &width, &height); - - if (m_Data.Width != width || m_Data.Height != height) + if (m_Data.SizeDirty) { - m_Data.Width = width; - m_Data.Height = height; - - m_SwapChain->OnResize(width, height); + m_Data.SizeDirty = false; + m_SwapChain->OnResize(m_Data.Width, m_Data.Height); } // Apply a pending VSync change. ProcessEvents runs when both the main and @@ -558,10 +558,10 @@ namespace Lux { glfwSetWindowAttrib(m_WindowHandle, GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE); } - void Window::BeginFrame() + bool Window::BeginFrame() { LUX_CORE_VERIFY(m_SwapChain); - m_SwapChain->BeginFrame(); + return m_SwapChain->BeginFrame(); } void Window::Maximize() diff --git a/Core/Source/Lux/Core/Window.h b/Core/Source/Lux/Core/Window.h index 0ddeded9..eee484c9 100644 --- a/Core/Source/Lux/Core/Window.h +++ b/Core/Source/Lux/Core/Window.h @@ -53,7 +53,7 @@ namespace Lux { virtual bool IsVSync() const; virtual void SetResizable(bool resizable) const; - void BeginFrame(); + bool BeginFrame(); virtual void Maximize(); virtual void CenterWindow(); @@ -94,6 +94,7 @@ namespace Lux { { std::string Title; uint32_t Width, Height; + bool SizeDirty = false; EventCallbackFn EventCallback; }; diff --git a/Core/Source/Lux/Editor/EditorCamera.cpp b/Core/Source/Lux/Editor/EditorCamera.cpp index f7f578f8..67f25e39 100644 --- a/Core/Source/Lux/Editor/EditorCamera.cpp +++ b/Core/Source/Lux/Editor/EditorCamera.cpp @@ -36,7 +36,11 @@ namespace Lux { static void DisableMouse() { +#ifdef LUX_PLATFORM_LINUX + Input::SetCursorMode(CursorMode::Locked); +#else Input::SetCursorMode(CursorMode::Hidden); +#endif ImGuiEx::SetInputEnabled(false); } diff --git a/Core/Source/Lux/ImGui/ImGuiLayer.cpp b/Core/Source/Lux/ImGui/ImGuiLayer.cpp index 3bded64c..c499c387 100644 --- a/Core/Source/Lux/ImGui/ImGuiLayer.cpp +++ b/Core/Source/Lux/ImGui/ImGuiLayer.cpp @@ -37,7 +37,9 @@ namespace Lux { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking +#ifndef LUX_PLATFORM_LINUX io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows +#endif // Configure Fonts { diff --git a/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp index 97730567..a4873ca8 100644 --- a/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp +++ b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp @@ -355,10 +355,18 @@ namespace Lux { m_AcknowledgedMacros.merge(includer->GetParsedSpecialMacros()); #else // Linux resolves HLSL includes in the dxc CLI at compile time (via -I flags), so it - // skips the DXC-based preprocessor here. Still hash the raw source so the shader cache - // invalidates correctly when a shader changes. + // skips the DXC-based preprocessor here. Hash source + global macros so the shader + // cache invalidates when either changes. m_StagesMetadata[stage] = StageData{}; - m_StagesMetadata[stage].HashValue = Hash::GenerateFNVHash(shaderSource); + std::string hashInput = shaderSource; + for (const auto& [name, value] : Renderer::GetGlobalShaderMacros()) + { + hashInput += name; + hashInput += '='; + hashInput += value; + hashInput += ';'; + } + m_StagesMetadata[stage].HashValue = Hash::GenerateFNVHash(hashInput); #endif } return shaderSources; @@ -479,6 +487,19 @@ namespace Lux { "-Fo", outTempName }; + const auto& globalMacros = Renderer::GetGlobalShaderMacros(); + std::vector macroDefs; + macroDefs.reserve(globalMacros.size()); + for (const auto& [name, value] : globalMacros) + { + exec.push_back("-D"); + if (value.size()) + macroDefs.push_back(std::format("{}={}", name, value)); + else + macroDefs.push_back(name); + exec.push_back(macroDefs.back().c_str()); + } + if (options.GenerateDebugInfo) { exec.push_back("-Qembed_debug"); @@ -779,7 +800,7 @@ namespace Lux { serializer.ReadRaw(header); - bool validHeader = memcmp(&header, "HZSR", 4) == 0; + bool validHeader = memcmp(&header, "LXSR", 4) == 0; LUX_CORE_VERIFY(validHeader); if (!validHeader) return false; diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp index 88f89c12..4a6850f9 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp @@ -42,6 +42,17 @@ namespace Lux { VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::Get().GetGraphicsDeviceManager(); + // Query actual surface pixel dimensions — on Wayland with HiDPI the + // caller may pass screen-coordinate sizes that differ from the real + // framebuffer extent. + vk::SurfaceCapabilitiesKHR surfaceCaps; + vk::Result capsRes = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfaceCapabilitiesKHR(m_Surface, &surfaceCaps); + if (capsRes == vk::Result::eSuccess && surfaceCaps.currentExtent.width != 0xFFFFFFFF) + { + m_Width = surfaceCaps.currentExtent.width; + m_Height = surfaceCaps.currentExtent.height; + } + m_SwapChainFormat = { vk::Format(nvrhi::vulkan::convertFormat(deviceParams.swapChainFormat)), vk::ColorSpaceKHR::eSrgbNonlinear @@ -228,14 +239,24 @@ namespace Lux { m_AcquiredSemaphore = semaphore; - if (res == vk::Result::eErrorOutOfDateKHR && attempt < maxAttempts) + if ((res == vk::Result::eErrorOutOfDateKHR || res == vk::Result::eSuboptimalKHR) && attempt < maxAttempts) { BackBufferResizing(); - auto surfaceCaps = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfaceCapabilitiesKHR(m_Surface); + + vk::SurfaceCapabilitiesKHR surfaceCaps; + vk::Result capsRes = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfaceCapabilitiesKHR(m_Surface, &surfaceCaps); + if (capsRes != vk::Result::eSuccess) + { + LUX_CORE_ERROR("VulkanSwapChain::BeginFrame - getSurfaceCapabilitiesKHR failed: {}", (int)capsRes); + return false; + } m_Width = surfaceCaps.currentExtent.width; m_Height = surfaceCaps.currentExtent.height; + if (m_Width == 0 || m_Height == 0) + return false; + Resize(); BackBufferResized(); } @@ -247,7 +268,7 @@ namespace Lux { m_AcquireSemaphoreIndex = (m_AcquireSemaphoreIndex + 1) % m_AcquireSemaphores.size(); - return res == vk::Result::eSuccess; + return res == vk::Result::eSuccess || res == vk::Result::eSuboptimalKHR; } void VulkanSwapChain::Present() @@ -275,7 +296,7 @@ namespace Lux { .setPImageIndices(&m_SwapChainIndex); const vk::Result res = vulkanDeviceManager->m_PresentQueue.presentKHR(&info); - LUX_CORE_VERIFY(res == vk::Result::eSuccess || res == vk::Result::eErrorOutOfDateKHR); + LUX_CORE_VERIFY(res == vk::Result::eSuccess || res == vk::Result::eSuboptimalKHR || res == vk::Result::eErrorOutOfDateKHR); } RenderCommandBuffer::UnlockQueue(); diff --git a/Core/Source/Lux/Renderer/Renderer.cpp b/Core/Source/Lux/Renderer/Renderer.cpp index 67f778d9..0115ee1a 100644 --- a/Core/Source/Lux/Renderer/Renderer.cpp +++ b/Core/Source/Lux/Renderer/Renderer.cpp @@ -1808,6 +1808,8 @@ namespace Lux { Renderer::Submit([renderCommandBuffer, image, clearColor, subresourceSet]() mutable { nvrhi::CommandListHandle commandList = renderCommandBuffer->GetActive(); + if (!commandList || !image || !image->GetHandle()) + return; const auto& spec = image->GetSpecification(); const std::string markerName = "ClearImage: " + (spec.DebugName.empty() ? std::string("Image2D") : spec.DebugName); renderCommandBuffer->RT_BeginMarker(markerName); diff --git a/Core/Source/Lux/Scripting/ScriptEntityStorage.cpp b/Core/Source/Lux/Scripting/ScriptEntityStorage.cpp index a51cad61..350b732e 100644 --- a/Core/Source/Lux/Scripting/ScriptEntityStorage.cpp +++ b/Core/Source/Lux/Scripting/ScriptEntityStorage.cpp @@ -1,5 +1,6 @@ #include "lpch.h" #include "ScriptEntityStorage.hpp" +#include "Lux/Scene/Scene.h" #include "ScriptEngine.h" namespace Lux { diff --git a/Core/vendor/nvrhi b/Core/vendor/nvrhi index 360ad30f..b5f74cc0 160000 --- a/Core/vendor/nvrhi +++ b/Core/vendor/nvrhi @@ -1 +1 @@ -Subproject commit 360ad30f53fbae3d9f4eff524f4cbe11105232fb +Subproject commit b5f74cc06f27812c13919953c06e18048501cdb5 diff --git a/Dependencies.lua b/Dependencies.lua index 955e485c..85e6fd90 100644 --- a/Dependencies.lua +++ b/Dependencies.lua @@ -5,8 +5,9 @@ function firstToUpper(str) return (str:gsub("^%l", string.upper)) end --- Grab Vulkan SDK path -VULKAN_SDK = os.getenv("VULKAN_SDK") +-- Grab Vulkan SDK path (fall back to the repo-bundled SDK so premake generation +-- works without the environment variable that Linux-Run.sh sets at runtime). +VULKAN_SDK = os.getenv("VULKAN_SDK") or path.getabsolute("Core/vendor/VulkanSDK/x86_64") --[[ If you're adding a new dependency all you have to do to get it linking diff --git a/Editor/LuxSampleProject/LuxSample.luxproj b/Editor/LuxSampleProject/LuxSample.luxproj index c0e72617..fb71bdb7 100644 --- a/Editor/LuxSampleProject/LuxSample.luxproj +++ b/Editor/LuxSampleProject/LuxSample.luxproj @@ -19,7 +19,7 @@ Project: Fullscreen: true VSync: true IconPath: Textures/luxLogo.png - IconHandle: 10706518493978196728 + IconHandle: 12258613209527478073 TargetConfig: Release SceneRenderer: Rendering: diff --git a/Editor/Source/EditorLayer.cpp b/Editor/Source/EditorLayer.cpp index 20b58209..072451bf 100644 --- a/Editor/Source/EditorLayer.cpp +++ b/Editor/Source/EditorLayer.cpp @@ -1263,23 +1263,10 @@ namespace Lux { drawList->AddImage(GetImGuiTextureID(EditorResources::HazelLogoTexture), logoMin, logoMax); } - const float menuBarX = 16.0f * 2.0f + 41.0f; - ImGui::SetCursorPos(ImVec2(menuBarX, 4.0f)); - UI_DrawMenubar(); - - const std::string sceneName = GetSceneDisplayName(m_EditorScenePath); - const ImVec2 sceneNameSize = ImGui::CalcTextSize(sceneName.c_str()); - const float sceneNameX = windowPos.x + (window->Size.x - sceneNameSize.x) * 0.5f; - const float sceneNameY = windowPos.y + (m_TitlebarHeight - sceneNameSize.y) * 0.5f; - drawList->AddText(ImVec2(sceneNameX, sceneNameY), Colors::Theme::textBrighter, sceneName.c_str()); - drawList->AddLine( - ImVec2(sceneNameX - 6.0f, sceneNameY + sceneNameSize.y + 4.0f), - ImVec2(sceneNameX + sceneNameSize.x + 6.0f, sceneNameY + sceneNameSize.y + 4.0f), - Colors::Theme::accent, 1.5f); - GLFWwindow* nativeWindow = Application::Get().GetWindow().GetNativeWindow(); const bool isMaximized = nativeWindow && glfwGetWindowAttrib(nativeWindow, GLFW_MAXIMIZED); + const float menuBarX = 16.0f * 2.0f + 41.0f; const float iconWidth = 14.0f; const float iconHeight = 14.0f; const float buttonWidth = 46.0f; @@ -1290,6 +1277,9 @@ namespace Lux { const float minimizeButtonX = maximizeButtonX - buttonWidth; const float titlebarGap = 12.0f; + const std::string sceneName = GetSceneDisplayName(m_EditorScenePath); + const ImVec2 sceneNameSize = ImGui::CalcTextSize(sceneName.c_str()); + const std::string projectName = GetProjectDisplayName(); const float projectBoxPaddingX = 10.0f; const float projectBoxHeight = 26.0f; @@ -1329,22 +1319,41 @@ namespace Lux { const float projectBoxMinX = drawProjectBox ? projectBoxMaxX - projectBoxWidth : projectBoxMaxX; const float dragZoneMinX = 70.0f; const float dragZoneMaxX = std::max(dragZoneMinX, (drawProjectBox ? projectBoxMinX : minimizeButtonX) - titlebarGap); - ImGui::SetCursorPos(ImVec2(dragZoneMinX, 0.0f)); - ImGui::InvisibleButton("##titleBarDragZone", ImVec2(std::max(0.0f, dragZoneMaxX - dragZoneMinX), m_TitlebarHeight)); - const ImVec2 dragMin = ImGui::GetItemRectMin(); - const ImVec2 dragMax = ImGui::GetItemRectMax(); - m_TitleBarDragRectMin = ImVec2(dragMin.x - windowPos.x, dragMin.y - windowPos.y); - m_TitleBarDragRectMax = ImVec2(dragMax.x - windowPos.x, dragMax.y - windowPos.y); + +#ifdef LUX_PLATFORM_LINUX + // On Linux/Wayland, the compositor handles window dragging via + // glfwSetTitlebarHitTestCallback — no InvisibleButton needed. + // Draw the menu bar first, then set the drag zone to start AFTER it + // so menu clicks aren't intercepted as window drags. + ImGui::SetCursorPos(ImVec2(menuBarX, 4.0f)); + UI_DrawMenubar(); + const float menuBarRight = ImGui::GetItemRectMax().x - windowPos.x; + m_TitleBarDragRectMin = ImVec2(menuBarRight, 0.0f); + m_TitleBarDragRectMax = ImVec2(dragZoneMaxX, m_TitlebarHeight); +#else m_TitleBarDragRectMin = ImVec2(dragZoneMinX, 0.0f); m_TitleBarDragRectMax = ImVec2(dragZoneMaxX, m_TitlebarHeight); -#if !defined(LUX_PLATFORM_WINDOWS) - if (nativeWindow && !isMaximized && ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + ImGui::SetNextItemAllowOverlap(); + ImGui::SetCursorPos(ImVec2(dragZoneMinX, 0.0f)); + ImGui::InvisibleButton("##titleBarDragZone", ImVec2(std::max(0.0f, dragZoneMaxX - dragZoneMinX), m_TitlebarHeight)); + + ImGui::SuspendLayout(); { - glfwDragWindow(nativeWindow); + ImGui::SetCursorPos(ImVec2(menuBarX, 4.0f)); + UI_DrawMenubar(); } + ImGui::ResumeLayout(); #endif + const float sceneNameX = windowPos.x + (window->Size.x - sceneNameSize.x) * 0.5f; + const float sceneNameY = windowPos.y + (m_TitlebarHeight - sceneNameSize.y) * 0.5f; + drawList->AddText(ImVec2(sceneNameX, sceneNameY), Colors::Theme::textBrighter, sceneName.c_str()); + drawList->AddLine( + ImVec2(sceneNameX - 6.0f, sceneNameY + sceneNameSize.y + 4.0f), + ImVec2(sceneNameX + sceneNameSize.x + 6.0f, sceneNameY + sceneNameSize.y + 4.0f), + Colors::Theme::accent, 1.5f); + if (drawProjectBox) { const ImVec2 projectBoxMin(windowPos.x + projectBoxMinX, windowPos.y + 14.0f); diff --git a/Editor/Source/LuxEditorApp.cpp b/Editor/Source/LuxEditorApp.cpp index 0a9c1b82..d413ad96 100644 --- a/Editor/Source/LuxEditorApp.cpp +++ b/Editor/Source/LuxEditorApp.cpp @@ -54,9 +54,15 @@ namespace Lux { // Threading policy is a user setting persisted in App.lsettings (read here because the // RenderThread is constructed with it before the Application object exists). Defaults to // multi-threaded; the "Application Settings" panel lets the user force single-threaded. + // On Linux, default to single-threaded to avoid render thread race conditions with + // Wayland/Vulkan swapchain management until those are resolved. { Lux::ApplicationSettings settings("App.lsettings"); +#ifdef LUX_PLATFORM_LINUX + specification.CoreThreadingPolicy = Lux::ThreadingPolicyFromString(settings.Get("Core.ThreadingPolicy", "Single")); +#else specification.CoreThreadingPolicy = Lux::ThreadingPolicyFromString(settings.Get("Core.ThreadingPolicy", "Multi")); +#endif } return new LuxEditor(specification); diff --git a/premake5.lua b/premake5.lua index 84278573..37e4cbd5 100644 --- a/premake5.lua +++ b/premake5.lua @@ -91,6 +91,11 @@ workspace "Lux" filter "system:windows" buildoptions { "/EHsc", "/Zc:preprocessor", "/Zc:__cplusplus" } + filter "system:linux" + buildoptions { "-Wno-changes-meaning", "-Wno-delete-incomplete" } + + filter {} + outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}" group "Dependencies" @@ -103,6 +108,13 @@ group "Dependencies" include "Core/vendor/Coral/Coral.Native" include "Core/vendor/Coral/Coral.Managed" + -- Tracy's TracyFastVector.hpp uses memcpy in a template that gets instantiated + -- before is fully parsed on GCC. Force-include to fix. + project "Tracy" + filter "system:linux" + forceincludes { "cstring" } + filter {} + -- Coral's upstream premake only defines Debug/Release, and Coral.Managed dependson a -- Coral.Generator project that doesn't exist at our pinned commit. Rather than patch the -- vendored submodule (those edits wouldn't travel with the repo / would break fresh @@ -135,7 +147,42 @@ group "Dependencies/Text" group "" group "Dependencies/Renderer" + -- nvrhi's cmake-branch premake5.lua expects these symbols from the original Hazel + -- build system. Define them here so we don't have to modify the submodule. + HazelRootDirectory = path.getabsolute("scripts/compat") + + function DefaultTargetParams(preserveFilter) + filter "configurations:Debug or configurations:Debug-AS" + runtime "Debug" + filter "configurations:Release or configurations:Dist" + runtime "Release" + if preserveFilter then + filter {} + end + end + include "Core/vendor/nvrhi" + + -- Override nvrhi projects to add Vulkan headers, defines, and fix X11 macro pollution. + -- Use the same VULKAN_SDK path as Dependencies.lua so nvrhi and Core compile against + -- the same Vulkan header version (avoids C++ wrapper ABI mismatches). + project "NVRHI-Vulkan" + defines { "NVRHI_WITH_RTXMU=1" } + filter "system:windows" + defines { "VK_USE_PLATFORM_WIN32_KHR" } + includedirs { "%{VULKAN_SDK}/Include" } + filter "system:linux" + includedirs { "%{VULKAN_SDK}/include" } + filter {} + + project "NVRHI-D3D11" + defines { "NVRHI_WITH_RTXMU=1" } + + project "NVRHI-D3D12" + defines { "NVRHI_WITH_RTXMU=1" } + + project "NVRHI" + defines { "NVRHI_WITH_RTXMU=1" } group "" group "Core" diff --git a/scripts/Linux-Run.sh b/scripts/Linux-Run.sh old mode 100644 new mode 100755 diff --git a/scripts/compat/Hazel-ScriptCore/Source/Dummy.cpp b/scripts/compat/Hazel-ScriptCore/Source/Dummy.cpp new file mode 100644 index 00000000..4e4d6c59 --- /dev/null +++ b/scripts/compat/Hazel-ScriptCore/Source/Dummy.cpp @@ -0,0 +1 @@ +// Placeholder so D3D11/D3D12 static libraries have at least one translation unit on non-Windows. From e8a94645dab10cbc8919c17e7463490566561317 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Mon, 27 Jul 2026 19:38:34 -0400 Subject: [PATCH 09/20] Ignore editor state files (.lsettings, .lmesh, imgui.ini) Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_018SYjJnCeBqSsQCxSguSKSC --- .gitignore | 2 ++ Editor/App.lsettings | 17 ------------- Editor/imgui.ini | 58 ++++++++++++++++++++++---------------------- 3 files changed, 31 insertions(+), 46 deletions(-) delete mode 100644 Editor/App.lsettings diff --git a/.gitignore b/.gitignore index ef31f253..1cf5d46e 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ Editor/LuxSampleProject/Assets/AssetRegistry.lzr Core/vendor/JoltPhysics/bin/Debug-windows-x86_64/JoltPhysics/JoltPhysics.idb AGENTS.md *.lmat +*.lmesh +*.lsettings Editor/assets/AssetRegistry.lzr Editor/dbg_trace.txt Editor/dbg_ctor.txt diff --git a/Editor/App.lsettings b/Editor/App.lsettings deleted file mode 100644 index 2be2a009..00000000 --- a/Editor/App.lsettings +++ /dev/null @@ -1,17 +0,0 @@ -Lux Application Settings: - ContentBrowser.ShowAssetTypes: 1 - ContentBrowser.ThumbnailSize: 101.000000 - Discord.ApplicationID: 1528995178407399654 - Discord.RichPresenceEnabled: true - Editor.AutoOpenMostRecentProject: 1 - Editor.RotationSnapValue: 10.000000 - Editor.ShowBoundingBoxes: 0 - Editor.ShowEntityIcons: 0 - Editor.ShowPhysicsColliders: 0 - Editor.ShowRenderVolumes: 0 - Editor.ShowViewportPerformanceHUD: 1 - Editor.TranslationSnapValue: 0.500000 - Editor.UseGizmoSnap: 1 - Editor.VSync: 0 - RecentProjects.0: M:/Git/luxengine/Editor/LuxSampleProject/LuxSample.luxproj - RecentProjects.Count: 1 \ No newline at end of file diff --git a/Editor/imgui.ini b/Editor/imgui.ini index 15ba962c..27fb747f 100644 --- a/Editor/imgui.ini +++ b/Editor/imgui.ini @@ -1,6 +1,6 @@ [Window][Lux Editor] Pos=0,0 -Size=3840,2088 +Size=1920,1006 Collapsed=0 [Window][Debug##Default] @@ -9,8 +9,8 @@ Size=400,400 Collapsed=0 [Window][Light Settings] -Pos=0,1497 -Size=921,591 +Pos=0,511 +Size=370,495 Collapsed=0 DockId=0x00000011,0 @@ -21,26 +21,26 @@ Collapsed=0 DockId=0x0000000C,0 [Window][Content Browser] -Pos=923,1585 -Size=2414,503 +Pos=372,751 +Size=1084,255 Collapsed=0 DockId=0x00000002,0 [Window][Scene Hierarchy] -Pos=3339,57 -Size=501,850 +Pos=1458,57 +Size=462,395 Collapsed=0 DockId=0x00000005,0 [Window][Properties] -Pos=3339,909 -Size=501,1179 +Pos=1458,454 +Size=462,552 Collapsed=0 DockId=0x00000006,0 [Window][Scene Renderer] Pos=0,57 -Size=921,968 +Size=370,452 Collapsed=0 DockId=0x00000009,0 @@ -51,8 +51,8 @@ Collapsed=0 DockId=0x00000002,1 [Window][Viewport] -Pos=923,57 -Size=2414,1526 +Pos=372,57 +Size=1084,692 Collapsed=0 DockId=0x00000001,0 @@ -75,8 +75,8 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Text Editor] -Pos=923,57 -Size=2414,1526 +Pos=372,57 +Size=1084,692 Collapsed=0 DockId=0x00000001,1 @@ -91,8 +91,8 @@ Size=120,32 Collapsed=0 [Window][About LuxEngine] -Pos=1845,577 -Size=149,168 +Pos=903,36 +Size=113,198 Collapsed=0 [Window][Dear ImGui Metrics/Debugger] @@ -101,8 +101,8 @@ Size=296,417 Collapsed=0 [Window][Log] -Pos=923,1585 -Size=2414,503 +Pos=372,751 +Size=1084,255 Collapsed=0 DockId=0x00000002,1 @@ -125,8 +125,8 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Project Settings] -Pos=0,1497 -Size=921,591 +Pos=0,511 +Size=370,495 Collapsed=0 DockId=0x00000011,1 @@ -210,10 +210,10 @@ Column 3 Width=220 Column 4 Weight=1.0000 [Docking][Data] -DockSpace ID=0x370560FF Window=0xC4B82F3D Pos=0,57 Size=3840,2031 Split=Y Selected=0xC450F867 +DockSpace ID=0x370560FF Window=0xC4B82F3D Pos=0,57 Size=1920,949 Split=Y Selected=0xC450F867 DockNode ID=0x0000000F Parent=0x370560FF SizeRef=3852,256 Selected=0x97330144 DockNode ID=0x00000010 Parent=0x370560FF SizeRef=3852,1826 Split=X - DockNode ID=0x00000007 Parent=0x00000010 SizeRef=921,2084 Split=Y Selected=0x76EEA74C + DockNode ID=0x00000007 Parent=0x00000010 SizeRef=370,2084 Split=Y Selected=0x76EEA74C DockNode ID=0x00000009 Parent=0x00000007 SizeRef=581,968 Selected=0x68D924E0 DockNode ID=0x0000000A Parent=0x00000007 SizeRef=581,1061 Split=Y Selected=0x76EEA74C DockNode ID=0x0000000B Parent=0x0000000A SizeRef=581,705 Split=Y Selected=0x76EEA74C @@ -222,11 +222,11 @@ DockSpace ID=0x370560FF Window=0xC4B82F3D Pos=0,57 Size=3840,2031 Sp DockNode ID=0x00000011 Parent=0x0000000E SizeRef=921,564 Selected=0xDA40672E DockNode ID=0x00000012 Parent=0x0000000E SizeRef=921,495 Selected=0x91FB5A5B DockNode ID=0x0000000C Parent=0x0000000A SizeRef=581,710 Selected=0x3D0FF072 - DockNode ID=0x00000008 Parent=0x00000010 SizeRef=2917,2084 Split=X - DockNode ID=0x00000003 Parent=0x00000008 SizeRef=2414,2084 Split=Y - DockNode ID=0x00000001 Parent=0x00000003 SizeRef=3852,1526 CentralNode=1 Selected=0xC450F867 - DockNode ID=0x00000002 Parent=0x00000003 SizeRef=3852,503 Selected=0x3DF3100E - DockNode ID=0x00000004 Parent=0x00000008 SizeRef=501,2084 Split=Y Selected=0xB8729153 - DockNode ID=0x00000005 Parent=0x00000004 SizeRef=363,850 Selected=0xB8729153 - DockNode ID=0x00000006 Parent=0x00000004 SizeRef=363,1179 Selected=0x8C72BEA8 + DockNode ID=0x00000008 Parent=0x00000010 SizeRef=578,2084 Split=X + DockNode ID=0x00000003 Parent=0x00000008 SizeRef=1084,2084 Split=Y + DockNode ID=0x00000001 Parent=0x00000003 SizeRef=3852,692 CentralNode=1 Selected=0xC450F867 + DockNode ID=0x00000002 Parent=0x00000003 SizeRef=3852,255 Selected=0x3DF3100E + DockNode ID=0x00000004 Parent=0x00000008 SizeRef=462,2084 Split=Y Selected=0x8C72BEA8 + DockNode ID=0x00000005 Parent=0x00000004 SizeRef=184,351 Selected=0xB8729153 + DockNode ID=0x00000006 Parent=0x00000004 SizeRef=184,490 Selected=0x8C72BEA8 From f3120f5a2b859bbf0ce014250c2aff9e1b854015 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Mon, 27 Jul 2026 22:32:39 -0400 Subject: [PATCH 10/20] Linux distribution: runtime export, swap chain fixes, memory tracking, and packaging - Port runtime export system to Linux: extract shared helpers into RuntimeExportUtils, add Linux platform branches for .so bundling, launch script, and .desktop file generation - Fix Vulkan swap chain creation: use surface capabilities for preTransform, compositeAlpha, image count, present mode, and usage flags instead of hardcoded values; validate present mode against supported modes; fall back to FIFO when Immediate unsupported; pass VSync from window spec to device params - Fix VulkanContext hardcoded VK_KHR_xcb_surface with glfwGetRequiredInstanceExtensions for Wayland/X11 portability - Enable memory tracking on Linux with standard C++ operator new/delete overloads (no MSVC SAL annotations) - Add cross-platform DPI scaling via glfwGetWindowContentScale and content scale callback - Add RPATH ($ORIGIN/lib) to Editor and Lux-Runtime for portable shared library loading - Add Lux-Runtime to Linux build script and premake with X11, --start-group, LinkNethost, and postbuild commands - Skip ScriptCore gmake target on Linux (built via dotnet CLI) - Add Linux-RunRuntime.sh, .desktop file, and AppImage build script Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_018SYjJnCeBqSsQCxSguSKSC --- Core/Source/Lux/Core/Memory.cpp | 73 ++- Core/Source/Lux/Core/Memory.h | 27 +- Core/Source/Lux/Core/Ref.h | 3 +- Core/Source/Lux/Core/Window.cpp | 16 +- Core/Source/Lux/Core/Window.h | 1 + .../Lux/Platform/Vulkan/VulkanContext.cpp | 10 +- .../Lux/Platform/Vulkan/VulkanSwapChain.cpp | 80 +++- Core/Source/Lux/Renderer/DeviceManager.cpp | 18 +- Core/Source/Lux/Renderer/DeviceManager.h | 5 + Editor/Source/EditorLayer.cpp | 443 ++++-------------- .../Source/Panels/ProjectSettingsWindow.cpp | 216 +-------- Editor/Source/RuntimeExportUtils.cpp | 380 +++++++++++++++ Editor/Source/RuntimeExportUtils.h | 55 +++ Editor/premake5.lua | 2 +- Lux-Runtime/premake5.lua | 18 +- ScriptCore/premake5.lua | 3 + packaging/linux/build-appimage.sh | 133 ++++++ packaging/linux/lux-editor.desktop | 9 + scripts/Linux-Build.sh | 1 + scripts/Linux-RunRuntime.sh | 44 ++ 20 files changed, 924 insertions(+), 613 deletions(-) create mode 100644 Editor/Source/RuntimeExportUtils.cpp create mode 100644 Editor/Source/RuntimeExportUtils.h create mode 100755 packaging/linux/build-appimage.sh create mode 100644 packaging/linux/lux-editor.desktop create mode 100755 scripts/Linux-RunRuntime.sh diff --git a/Core/Source/Lux/Core/Memory.cpp b/Core/Source/Lux/Core/Memory.cpp index cdbabbb5..04277953 100644 --- a/Core/Source/Lux/Core/Memory.cpp +++ b/Core/Source/Lux/Core/Memory.cpp @@ -200,7 +200,9 @@ namespace Lux { } } -#if LUX_TRACK_MEMORY && LUX_PLATFORM_WINDOWS +#if LUX_TRACK_MEMORY + +#ifdef LUX_PLATFORM_WINDOWS _NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR void* __CRTDECL operator new(size_t size) @@ -273,4 +275,73 @@ void __CRTDECL operator delete[](void* memory, const char* file, int line) return Lux::Allocator::Free(memory); } +#elif defined(LUX_PLATFORM_LINUX) + +void* operator new(size_t size) +{ + return Lux::Allocator::Allocate(size); +} + +void* operator new[](size_t size) +{ + return Lux::Allocator::Allocate(size); +} + +void* operator new(size_t size, const char* desc) +{ + return Lux::Allocator::Allocate(size, desc); +} + +void* operator new[](size_t size, const char* desc) +{ + return Lux::Allocator::Allocate(size, desc); +} + +void* operator new(size_t size, const char* file, int line) +{ + return Lux::Allocator::Allocate(size, file, line); +} + +void* operator new[](size_t size, const char* file, int line) +{ + return Lux::Allocator::Allocate(size, file, line); +} + +void operator delete(void* memory) noexcept +{ + Lux::Allocator::Free(memory); +} + +void operator delete(void* memory, size_t size) noexcept +{ + Lux::Allocator::Free(memory, size); +} + +void operator delete(void* memory, const char* desc) +{ + Lux::Allocator::Free(memory); +} + +void operator delete(void* memory, const char* file, int line) +{ + Lux::Allocator::Free(memory); +} + +void operator delete[](void* memory) noexcept +{ + Lux::Allocator::Free(memory); +} + +void operator delete[](void* memory, const char* desc) +{ + Lux::Allocator::Free(memory); +} + +void operator delete[](void* memory, const char* file, int line) +{ + Lux::Allocator::Free(memory); +} + +#endif + #endif diff --git a/Core/Source/Lux/Core/Memory.h b/Core/Source/Lux/Core/Memory.h index b253b5a3..eb749cb9 100644 --- a/Core/Source/Lux/Core/Memory.h +++ b/Core/Source/Lux/Core/Memory.h @@ -117,17 +117,28 @@ void __CRTDECL operator delete[](void* memory); void __CRTDECL operator delete[](void* memory, const char* desc); void __CRTDECL operator delete[](void* memory, const char* file, int line); -#define lnew new(__FILE__, __LINE__) -#define ldelete delete - -#else -// Memory tracking relies on MSVC-specific global operator new/delete overloads, so it's a no-op -// on non-Windows platforms. lnew/ldelete fall back to plain new/delete (tracking simply disabled). -#define lnew new -#define ldelete delete +#elif defined(LUX_PLATFORM_LINUX) + +[[nodiscard]] void* operator new(size_t size); +[[nodiscard]] void* operator new[](size_t size); +[[nodiscard]] void* operator new(size_t size, const char* desc); +[[nodiscard]] void* operator new[](size_t size, const char* desc); +[[nodiscard]] void* operator new(size_t size, const char* file, int line); +[[nodiscard]] void* operator new[](size_t size, const char* file, int line); + +void operator delete(void* memory) noexcept; +void operator delete(void* memory, size_t size) noexcept; +void operator delete(void* memory, const char* desc); +void operator delete(void* memory, const char* file, int line); +void operator delete[](void* memory) noexcept; +void operator delete[](void* memory, const char* desc); +void operator delete[](void* memory, const char* file, int line); #endif +#define lnew new(__FILE__, __LINE__) +#define ldelete delete + #else #define lnew new diff --git a/Core/Source/Lux/Core/Ref.h b/Core/Source/Lux/Core/Ref.h index 8a26888d..5cb010cb 100644 --- a/Core/Source/Lux/Core/Ref.h +++ b/Core/Source/Lux/Core/Ref.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace Lux { @@ -170,7 +171,7 @@ namespace Lux { template static Ref Create(Args&&... args) { -#if LUX_TRACK_MEMORY && defined(LUX_PLATFORM_WINDOWS) +#if LUX_TRACK_MEMORY return Ref(new(typeid(T).name()) T(std::forward(args)...)); #else return Ref(new T(std::forward(args)...)); diff --git a/Core/Source/Lux/Core/Window.cpp b/Core/Source/Lux/Core/Window.cpp index 9d32b82e..88f7b518 100644 --- a/Core/Source/Lux/Core/Window.cpp +++ b/Core/Source/Lux/Core/Window.cpp @@ -114,7 +114,7 @@ namespace Lux { deviceParams.maxFramesInFlight = 2; deviceParams.backBufferWidth = m_Specification.Width; deviceParams.backBufferHeight = m_Specification.Height; - deviceParams.vsyncEnabled = false; + deviceParams.vsyncEnabled = m_Specification.VSync; // The Khronos validation layer intercepts every Vulkan call — a large CPU tax in // draw-heavy scenes. Keep it only in Debug builds; Release/Dist (where FPS is // measured and shipped) run without it. @@ -311,8 +311,22 @@ namespace Lux { m_SwapChain->Create(&m_Data.Width, &m_Data.Height, m_Specification.VSync); #endif //glfwMaximizeWindow(m_Window); + m_Data.Self = this; glfwSetWindowUserPointer(m_WindowHandle, &m_Data); + { + float xscale = 1.0f, yscale = 1.0f; + glfwGetWindowContentScale(m_WindowHandle, &xscale, &yscale); + m_DeviceManager->SetDPIScale(xscale, yscale); + } + + glfwSetWindowContentScaleCallback(m_WindowHandle, [](GLFWwindow* window, float xscale, float yscale) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + if (data.Self && data.Self->m_DeviceManager) + data.Self->m_DeviceManager->SetDPIScale(xscale, yscale); + }); + bool isRawMouseMotionSupported = glfwRawMouseMotionSupported(); if (isRawMouseMotionSupported) glfwSetInputMode(m_WindowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); diff --git a/Core/Source/Lux/Core/Window.h b/Core/Source/Lux/Core/Window.h index eee484c9..10f8ebcc 100644 --- a/Core/Source/Lux/Core/Window.h +++ b/Core/Source/Lux/Core/Window.h @@ -97,6 +97,7 @@ namespace Lux { bool SizeDirty = false; EventCallbackFn EventCallback; + Window* Self = nullptr; }; WindowData m_Data; diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanContext.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanContext.cpp index de07e818..ee744d15 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanContext.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanContext.cpp @@ -175,13 +175,9 @@ namespace Lux { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Extensions and Validation ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// TODO(Emily): GLFW can handle this for us -#ifdef LUX_PLATFORM_WINDOWS -#define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_win32_surface" -#elif defined(LUX_PLATFORM_LINUX) -#define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_xcb_surface" -#endif - std::vector instanceExtensions = { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME }; + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + std::vector instanceExtensions(glfwExtensions, glfwExtensions + glfwExtensionCount); instanceExtensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); // Very little performance hit, can be used in Release. if (s_Validation) { diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp index 4a6850f9..4c017491 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp @@ -42,17 +42,33 @@ namespace Lux { VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::Get().GetGraphicsDeviceManager(); - // Query actual surface pixel dimensions — on Wayland with HiDPI the - // caller may pass screen-coordinate sizes that differ from the real - // framebuffer extent. vk::SurfaceCapabilitiesKHR surfaceCaps; vk::Result capsRes = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfaceCapabilitiesKHR(m_Surface, &surfaceCaps); - if (capsRes == vk::Result::eSuccess && surfaceCaps.currentExtent.width != 0xFFFFFFFF) + if (capsRes != vk::Result::eSuccess) + { + LUX_CORE_ERROR("VulkanSwapChain::Create - getSurfaceCapabilitiesKHR failed: {}", nvrhi::vulkan::resultToString(VkResult(capsRes))); + return false; + } + + if (surfaceCaps.currentExtent.width != 0xFFFFFFFF) { m_Width = surfaceCaps.currentExtent.width; m_Height = surfaceCaps.currentExtent.height; } + if (m_Width == 0 || m_Height == 0) + { + LUX_CORE_WARN("VulkanSwapChain::Create - surface extent is 0x0, deferring swap chain creation."); + return false; + } + + LUX_CORE_INFO("VulkanSwapChain::Create - extent={}x{}, surfaceCaps: minImages={}, maxImages={}, supportedTransforms={:#x}, supportedCompositeAlpha={:#x}, supportedUsageFlags={:#x}", + m_Width, m_Height, + surfaceCaps.minImageCount, surfaceCaps.maxImageCount, + (uint32_t)surfaceCaps.supportedTransforms, + (uint32_t)surfaceCaps.supportedCompositeAlpha, + (uint32_t)surfaceCaps.supportedUsageFlags); + m_SwapChainFormat = { vk::Format(nvrhi::vulkan::convertFormat(deviceParams.swapChainFormat)), vk::ColorSpaceKHR::eSrgbNonlinear @@ -60,6 +76,14 @@ namespace Lux { vk::Extent2D extent = vk::Extent2D(m_Width, m_Height); + uint32_t minImages = deviceParams.swapChainBufferCount; + if (capsRes == vk::Result::eSuccess) + { + minImages = std::max(minImages, surfaceCaps.minImageCount); + if (surfaceCaps.maxImageCount > 0) + minImages = std::min(minImages, surfaceCaps.maxImageCount); + } + std::unordered_set uniqueQueues = { uint32_t(vulkanDeviceManager->m_QueueFamilyIndices.Graphics), uint32_t(vulkanDeviceManager->m_QueueFamilyIndices.Present) }; @@ -68,21 +92,53 @@ namespace Lux { const bool enableSwapChainSharing = queues.size() > 1; + vk::SurfaceTransformFlagBitsKHR preTransform = surfaceCaps.currentTransform; + + vk::CompositeAlphaFlagBitsKHR compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque; + if (!(surfaceCaps.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::eOpaque)) + { + if (surfaceCaps.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::eInherit) + compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eInherit; + else if (surfaceCaps.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::ePreMultiplied) + compositeAlpha = vk::CompositeAlphaFlagBitsKHR::ePreMultiplied; + else if (surfaceCaps.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::ePostMultiplied) + compositeAlpha = vk::CompositeAlphaFlagBitsKHR::ePostMultiplied; + } + + vk::ImageUsageFlags imageUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; + imageUsage &= surfaceCaps.supportedUsageFlags; + + vk::PresentModeKHR presentMode = deviceParams.vsyncEnabled ? vk::PresentModeKHR::eFifo : vk::PresentModeKHR::eImmediate; + { + auto availableModes = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfacePresentModesKHR(m_Surface); + bool modeSupported = false; + for (auto mode : availableModes) + { + if (mode == presentMode) + { + modeSupported = true; + break; + } + } + if (!modeSupported) + presentMode = vk::PresentModeKHR::eFifo; + } + auto desc = vk::SwapchainCreateInfoKHR() .setSurface(m_Surface) - .setMinImageCount(deviceParams.swapChainBufferCount) + .setMinImageCount(minImages) .setImageFormat(m_SwapChainFormat.format) .setImageColorSpace(m_SwapChainFormat.colorSpace) .setImageExtent(extent) .setImageArrayLayers(1) - .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled) + .setImageUsage(imageUsage) .setImageSharingMode(enableSwapChainSharing ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive) .setFlags(vulkanDeviceManager->m_SwapChainMutableFormatSupported ? vk::SwapchainCreateFlagBitsKHR::eMutableFormat : vk::SwapchainCreateFlagBitsKHR(0)) .setQueueFamilyIndexCount(enableSwapChainSharing ? uint32_t(queues.size()) : 0) .setPQueueFamilyIndices(enableSwapChainSharing ? queues.data() : nullptr) - .setPreTransform(vk::SurfaceTransformFlagBitsKHR::eIdentity) - .setCompositeAlpha(vk::CompositeAlphaFlagBitsKHR::eOpaque) - .setPresentMode(deviceParams.vsyncEnabled ? vk::PresentModeKHR::eFifo : vk::PresentModeKHR::eImmediate) + .setPreTransform(preTransform) + .setCompositeAlpha(compositeAlpha) + .setPresentMode(presentMode) .setClipped(true) .setOldSwapchain(nullptr); @@ -112,7 +168,11 @@ namespace Lux { const vk::Result res = vulkanDeviceManager->m_VulkanDevice.createSwapchainKHR(&desc, nullptr, &m_SwapChain); if (res != vk::Result::eSuccess) { - LUX_CORE_ERROR("Failed to create a Vulkan swap chain, error code = {}", nvrhi::vulkan::resultToString(VkResult(res))); + LUX_CORE_ERROR("Failed to create a Vulkan swap chain, error code = {}. extent={}x{}, minImages={}, format={}, preTransform={:#x}, compositeAlpha={:#x}, presentMode={}, mutableFormat={}", + nvrhi::vulkan::resultToString(VkResult(res)), + m_Width, m_Height, minImages, (int)m_SwapChainFormat.format, + (uint32_t)preTransform, (uint32_t)compositeAlpha, + (int)desc.presentMode, vulkanDeviceManager->m_SwapChainMutableFormatSupported); return false; } // retrieve swap chain images diff --git a/Core/Source/Lux/Renderer/DeviceManager.cpp b/Core/Source/Lux/Renderer/DeviceManager.cpp index 1d700611..5aa7cf74 100644 --- a/Core/Source/Lux/Renderer/DeviceManager.cpp +++ b/Core/Source/Lux/Renderer/DeviceManager.cpp @@ -307,20 +307,14 @@ const DeviceCreationParameters& DeviceManager::GetDeviceParams() void DeviceManager::WindowPosCallback(int x, int y) { -#if defined(LUX_PLATFORM_WINDOWS) - if (m_DeviceParams.enablePerMonitorDPI) + if (m_DeviceParams.enablePerMonitorDPI && m_WindowHandle) { - HWND hwnd = glfwGetWin32Window(m_WindowHandle); - auto monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - - unsigned int dpiX; - unsigned int dpiY; - GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); - - m_DPIScaleFactorX = dpiX / 96.f; - m_DPIScaleFactorY = dpiY / 96.f; + float xscale = 1.0f, yscale = 1.0f; + glfwGetWindowContentScale(m_WindowHandle, &xscale, &yscale); + m_DPIScaleFactorX = xscale; + m_DPIScaleFactorY = yscale; } -#endif + #if 0 if (m_EnableRenderDuringWindowMovement && m_SwapChainFramebuffers.size() > 0) diff --git a/Core/Source/Lux/Renderer/DeviceManager.h b/Core/Source/Lux/Renderer/DeviceManager.h index 6a4bbab8..7ec8a33b 100644 --- a/Core/Source/Lux/Renderer/DeviceManager.h +++ b/Core/Source/Lux/Renderer/DeviceManager.h @@ -158,6 +158,11 @@ namespace Lux x = m_DPIScaleFactorX; y = m_DPIScaleFactorY; } + void SetDPIScale(float x, float y) + { + m_DPIScaleFactorX = x; + m_DPIScaleFactorY = y; + } Window* GetWindowContext() { return m_LuxWindow; } void SetWindowContext(Window* window) { m_LuxWindow = window; } diff --git a/Editor/Source/EditorLayer.cpp b/Editor/Source/EditorLayer.cpp index 072451bf..7da9e0fc 100644 --- a/Editor/Source/EditorLayer.cpp +++ b/Editor/Source/EditorLayer.cpp @@ -1,4 +1,5 @@ #include "EditorLayer.h" +#include "RuntimeExportUtils.h" #include "Lux/Scene/SceneSerializer.h" #include "Lux/Core/Application.h" @@ -142,374 +143,32 @@ namespace Lux { return imguiRenderer->CreateFrameTexture(image->GetHandle().Get(), nvrhi::AllSubresources); } - constexpr const char* s_RuntimeProjectFile = "Project.luxruntime"; - constexpr const char* s_RuntimeAssetPackFile = "AssetPack.lap"; - constexpr const char* s_RuntimeShaderPackFile = "ShaderPack.lsp"; + using RuntimeExport::RuntimeProjectFile; + using RuntimeExport::RuntimeAssetPackFile; + using RuntimeExport::RuntimeShaderPackFile; + using RuntimeExport::FileExists; + using RuntimeExport::SanitizeBuildName; + using RuntimeExport::CopyFileIfExists; + using RuntimeExport::CopyDirectoryRecursive; + using RuntimeExport::FindFirstExistingDirectory; + using RuntimeExport::FindRepositoryRootFrom; + using RuntimeExport::GetRuntimeOutputDirectory; + using RuntimeExport::GetRuntimeExecutablePath; + using RuntimeExport::IsRuntimeExecutableOutdated; + using RuntimeExport::BuildRuntimeExecutable; + using RuntimeExport::ResolveScriptProjectFile; + using RuntimeExport::IsScriptModuleOutdated; + using RuntimeExport::BuildScriptModule; + + constexpr const char* s_RuntimeProjectFile = RuntimeProjectFile; + constexpr const char* s_RuntimeAssetPackFile = RuntimeAssetPackFile; + constexpr const char* s_RuntimeShaderPackFile = RuntimeShaderPackFile; constexpr RuntimeExportTarget s_RuntimeExportTargets[] = { RuntimeExportTarget::Debug, RuntimeExportTarget::Release, RuntimeExportTarget::Dist }; - std::string SanitizeBuildName(std::string value) - { - if (value.empty()) - value = "LuxGame"; - - for (char& c : value) - { - const bool valid = std::isalnum((unsigned char)c) || c == '-' || c == '_'; - if (!valid) - c = '_'; - } - - return value; - } - - bool CopyFileIfExists(const std::filesystem::path& source, const std::filesystem::path& destination, bool required = false) - { - std::error_code ec; - if (!std::filesystem::exists(source, ec) || ec) - { - if (required) - LUX_CONSOLE_LOG_ERROR("Missing export file: {}", source.string()); - return false; - } - - std::filesystem::create_directories(destination.parent_path(), ec); - ec.clear(); - std::filesystem::copy_file(source, destination, std::filesystem::copy_options::overwrite_existing, ec); - if (ec) - { - LUX_CONSOLE_LOG_ERROR("Failed to copy '{}' to '{}': {}", source.string(), destination.string(), ec.message()); - return false; - } - - return true; - } - - bool CopyDirectoryRecursive(const std::filesystem::path& source, const std::filesystem::path& destination, bool skipDebugFiles = false) - { - std::error_code ec; - if (!std::filesystem::exists(source, ec) || ec) - return false; - - for (const auto& entry : std::filesystem::recursive_directory_iterator(source, ec)) - { - if (ec) - break; - - const std::filesystem::path relativePath = std::filesystem::relative(entry.path(), source, ec); - if (ec) - continue; - - if (!relativePath.empty() && *relativePath.begin() == "Cache") - continue; - - const std::filesystem::path target = destination / relativePath; - if (entry.is_directory(ec)) - { - std::filesystem::create_directories(target, ec); - continue; - } - - if (entry.is_regular_file(ec)) - { - if (skipDebugFiles) - { - const std::filesystem::path extension = entry.path().extension(); - if (extension == ".pdb" || extension == ".ilk" || extension == ".exp") - continue; - } - CopyFileIfExists(entry.path(), target); - } - } - - return true; - } - - std::filesystem::path FindFirstExistingDirectory(std::initializer_list candidates) - { - std::error_code ec; - for (const std::filesystem::path& candidate : candidates) - { - if (!candidate.empty() && std::filesystem::exists(candidate, ec) && std::filesystem::is_directory(candidate, ec)) - return candidate; - } - - return {}; - } - - bool FileExists(const std::filesystem::path& path) - { - std::error_code ec; - return !path.empty() && std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); - } - - std::string QuotePowerShellArgument(std::string value) - { - std::string result = "'"; - for (char c : value) - { - if (c == '\'') - result += "''"; - else - result += c; - } - result += "'"; - return result; - } - - bool IsBuildConfigurationDirectory(const std::filesystem::path& path) - { - const std::string directoryName = path.filename().string(); - return path.parent_path().filename() == "bin" && directoryName.find("-windows-x86_64") != std::string::npos; - } - - std::filesystem::path GetRuntimeOutputDirectory(RuntimeExportTarget target) - { - return std::string(RuntimeExportTargetToString(target)) + "-windows-x86_64"; - } - - std::filesystem::path FindRepositoryRootFrom(std::filesystem::path start) - { - if (start.empty()) - return {}; - - std::error_code ec; - start = std::filesystem::absolute(start, ec).lexically_normal(); - if (ec) - return {}; - - if (std::filesystem::is_regular_file(start, ec)) - start = start.parent_path(); - - for (std::filesystem::path directory = start; !directory.empty(); directory = directory.parent_path()) - { - if (std::filesystem::exists(directory / "premake5.lua", ec) - && std::filesystem::exists(directory / "Core", ec) - && std::filesystem::exists(directory / "Lux-Runtime" / "premake5.lua", ec)) - { - return directory; - } - - if (directory == directory.root_path()) - break; - } - - return {}; - } - - bool IsRuntimeExecutableOutdated(const std::filesystem::path& runtimeExe, const std::filesystem::path& repositoryRoot) - { - std::error_code ec; - if (runtimeExe.empty() || !std::filesystem::exists(runtimeExe, ec)) - return true; - - const auto executableWriteTime = std::filesystem::last_write_time(runtimeExe, ec); - if (ec) - return true; - - const std::array sourceRoots = { - repositoryRoot / "Lux-Runtime", - repositoryRoot / "Core" / "Source" - }; - - for (const auto& sourceRoot : sourceRoots) - { - if (!std::filesystem::exists(sourceRoot, ec)) - continue; - - for (const auto& entry : std::filesystem::recursive_directory_iterator(sourceRoot, ec)) - { - if (ec) - break; - if (!entry.is_regular_file(ec)) - continue; - - const std::filesystem::path extension = entry.path().extension(); - if (extension != ".cpp" && extension != ".h" && extension != ".hpp" && extension != ".c" && extension != ".rc" && extension != ".lua") - continue; - - if (entry.last_write_time(ec) > executableWriteTime && !ec) - return true; - } - } - - return false; - } - - std::filesystem::path GetRuntimeExecutablePath(RuntimeExportTarget target) - { - std::error_code ec; - const std::filesystem::path current = std::filesystem::current_path(ec); - if (ec) - return {}; - - const std::filesystem::path runtimeOutputDirectory = GetRuntimeOutputDirectory(target); - std::vector candidates; - - if (std::filesystem::path root = FindRepositoryRootFrom(current); !root.empty()) - candidates.emplace_back((root / "bin" / runtimeOutputDirectory / "Lux-Runtime" / "Lux-Runtime.exe").lexically_normal()); - - if (Ref activeProject = Project::GetActive()) - { - if (std::filesystem::path root = FindRepositoryRootFrom(activeProject->GetProjectDirectory()); !root.empty()) - candidates.emplace_back((root / "bin" / runtimeOutputDirectory / "Lux-Runtime" / "Lux-Runtime.exe").lexically_normal()); - } - - const std::filesystem::path buildConfigDirectory = current.filename() == "Editor" ? current.parent_path() : current; - if (IsBuildConfigurationDirectory(buildConfigDirectory)) - candidates.emplace_back((buildConfigDirectory / "Lux-Runtime" / "Lux-Runtime.exe").lexically_normal()); - - for (const std::filesystem::path& candidate : candidates) - { - if (FileExists(candidate)) - return candidate; - } - - return {}; - } - - bool BuildRuntimeExecutable(RuntimeExportTarget target) - { - std::filesystem::path repositoryRoot = FindRepositoryRootFrom(std::filesystem::current_path()); - if (repositoryRoot.empty()) - { - if (Ref activeProject = Project::GetActive()) - repositoryRoot = FindRepositoryRootFrom(activeProject->GetProjectDirectory()); - } - - if (repositoryRoot.empty()) - { - LUX_CONSOLE_LOG_ERROR("Could not locate repository root for Lux-Runtime build."); - return false; - } - - const std::filesystem::path projectFile = repositoryRoot / "Lux-Runtime" / "Lux-Runtime.vcxproj"; - if (!FileExists(projectFile)) - { - LUX_CONSOLE_LOG_ERROR("Lux-Runtime project file not found: {}", projectFile.string()); - return false; - } - - const std::filesystem::path msbuildPath = "C:/Program Files/Microsoft Visual Studio/18/Community/MSBuild/Current/Bin/MSBuild.exe"; - const std::string msbuild = FileExists(msbuildPath) ? msbuildPath.string() : "MSBuild.exe"; - const std::string command = - "powershell -NoProfile -ExecutionPolicy Bypass -Command \"& " - + QuotePowerShellArgument(msbuild) + " " - + QuotePowerShellArgument(projectFile.string()) - + " /t:Build /p:Configuration=" + RuntimeExportTargetToString(target) - + " /p:Platform=x64 /m:1 /nr:false /v:minimal\""; - - LUX_CONSOLE_LOG_INFO("Building Lux-Runtime ({})...", RuntimeExportTargetToString(target)); - const int result = std::system(command.c_str()); - if (result != 0) - { - LUX_CONSOLE_LOG_ERROR("Lux-Runtime build failed with exit code {}.", result); - return false; - } - - LUX_CONSOLE_LOG_INFO("Lux-Runtime build complete."); - return true; - } - - std::filesystem::path ResolveScriptProjectFile(Ref project) - { - if (!project) - return {}; - - std::filesystem::path scriptProject = project->GetScriptProjectPath(); - if (FileExists(scriptProject)) - return scriptProject; - - std::filesystem::path scriptProjectFilename = project->GetConfig().ScriptModulePath.filename(); - if (!scriptProjectFilename.empty()) - { - scriptProjectFilename.replace_extension(".csproj"); - scriptProject = project->GetAssetDirectory() / "Scripts" / scriptProjectFilename; - } - - return scriptProject; - } - - bool IsScriptModuleOutdated(const std::filesystem::path& scriptModule, const std::filesystem::path& scriptProject) - { - std::error_code ec; - if (!FileExists(scriptModule)) - return true; - if (!FileExists(scriptProject)) - return false; - - const auto moduleWriteTime = std::filesystem::last_write_time(scriptModule, ec); - if (ec) - return true; - - ec.clear(); - if (std::filesystem::last_write_time(scriptProject, ec) > moduleWriteTime && !ec) - return true; - - const std::filesystem::path scriptsDirectory = scriptProject.parent_path(); - if (!std::filesystem::exists(scriptsDirectory, ec)) - return false; - - for (const auto& entry : std::filesystem::recursive_directory_iterator(scriptsDirectory, ec)) - { - if (ec) - break; - if (!entry.is_regular_file(ec)) - continue; - - const std::filesystem::path relativePath = std::filesystem::relative(entry.path(), scriptsDirectory, ec); - if (!ec && !relativePath.empty()) - { - const std::filesystem::path first = *relativePath.begin(); - if (first == "Binaries" || first == "Intermediates") - continue; - } - - const std::filesystem::path extension = entry.path().extension(); - if (extension != ".cs" && extension != ".csproj" && extension != ".props" && extension != ".targets" && extension != ".lua") - continue; - - ec.clear(); - if (entry.last_write_time(ec) > moduleWriteTime && !ec) - return true; - } - - return false; - } - - bool BuildScriptModule(RuntimeExportTarget target) - { - Ref project = Project::GetActive(); - if (!project) - { - LUX_CONSOLE_LOG_ERROR("No active project to build scripts for."); - return false; - } - - const std::filesystem::path scriptProject = ResolveScriptProjectFile(project); - if (!FileExists(scriptProject)) - { - LUX_CONSOLE_LOG_ERROR("Script project file not found: {}", scriptProject.string()); - return false; - } - - if (!ScriptBuilder::BuildProject(scriptProject, RuntimeExportTargetToString(target))) - return false; - - const std::filesystem::path scriptModule = project->GetScriptModuleFilePath(); - if (!FileExists(scriptModule)) - { - LUX_CONSOLE_LOG_ERROR("Script build completed, but the script module was not found: {}", scriptModule.string()); - return false; - } - - LUX_CONSOLE_LOG_INFO("Script build complete: {}", scriptModule.string()); - return true; - } - bool StartupSceneUsesScripts(Ref project) { if (!project || !project->GetConfig().StartSceneHandle) @@ -2550,7 +2209,7 @@ namespace Lux { LUX_CONSOLE_LOG_INFO("Runtime export preflight:"); LUX_CONSOLE_LOG_INFO(" Startup Scene: {}", hasStartupScene ? "set" : "missing"); LUX_CONSOLE_LOG_INFO(" Startup Scene Uses Scripts: {}", startupSceneUsesScripts ? "yes" : "no"); - LUX_CONSOLE_LOG_INFO(" Lux-Runtime.exe: {}", hasRuntimeExe ? runtimeExe.string() : "missing"); + LUX_CONSOLE_LOG_INFO(" {}: {}", RuntimeExport::RuntimeExeName, hasRuntimeExe ? runtimeExe.string() : "missing"); LUX_CONSOLE_LOG_INFO(" Resources: {}", hasResources ? resourcesSource.string() : "missing"); if (project->GetConfig().ScriptModulePath.empty()) LUX_CONSOLE_LOG_INFO(" Script Module: optional"); @@ -2578,7 +2237,7 @@ namespace Lux { LUX_CONSOLE_LOG_WARN("DotNet directory (Coral.Managed) is missing. Export will continue, but scripting will not run."); const std::string buildName = SanitizeBuildName(runtimeSettings.GameName.empty() ? project->GetConfig().Name : runtimeSettings.GameName); - const std::filesystem::path exportRoot = selectedFolder / (buildName + "-Windows-x86_64"); + const std::filesystem::path exportRoot = selectedFolder / (buildName + RuntimeExport::PlatformExportLabel); const std::filesystem::path exportAssets = exportRoot / "Assets"; std::filesystem::create_directories(exportAssets, ec); @@ -2608,13 +2267,30 @@ namespace Lux { return false; LUX_CONSOLE_LOG_INFO(" AssetPack.lap: created"); +#ifdef LUX_PLATFORM_LINUX + const std::filesystem::path exportedExe = exportRoot / buildName; +#else const std::filesystem::path exportedExe = exportRoot / (buildName + ".exe"); +#endif if (!CopyFileIfExists(runtimeExe, exportedExe, true)) LUX_CONSOLE_LOG_WARN("Build the Lux-Runtime project once before exporting a standalone executable."); const std::filesystem::path runtimeDirectory = runtimeExe.parent_path(); if (!runtimeDirectory.empty() && std::filesystem::exists(runtimeDirectory, ec)) { +#ifdef LUX_PLATFORM_LINUX + const std::filesystem::path libDir = exportRoot / "lib"; + std::filesystem::create_directories(libDir, ec); + for (const auto& entry : std::filesystem::directory_iterator(runtimeDirectory, ec)) + { + if (!entry.is_regular_file(ec)) + continue; + + const std::string filename = entry.path().filename().string(); + if (filename.find(".so") != std::string::npos) + CopyFileIfExists(entry.path(), libDir / entry.path().filename()); + } +#else for (const auto& entry : std::filesystem::directory_iterator(runtimeDirectory, ec)) { if (!entry.is_regular_file(ec)) @@ -2624,6 +2300,7 @@ namespace Lux { if (extension == ".dll" || (extension == ".pdb" && targetConfig != RuntimeExportTarget::Dist)) CopyFileIfExists(entry.path(), exportRoot / entry.path().filename()); } +#endif } if (!resourcesSource.empty()) @@ -2659,6 +2336,44 @@ namespace Lux { if (!WriteRuntimeSettingsFile(exportAssets / "RuntimeSettings.yaml", runtimeSettings, runtimeIconPath)) return false; +#ifdef LUX_PLATFORM_LINUX + std::filesystem::permissions(exportedExe, + std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, + std::filesystem::perm_options::add, ec); + + { + const std::filesystem::path launchScript = exportRoot / ("run-" + buildName + ".sh"); + std::ofstream script(launchScript); + if (script.is_open()) + { + script << "#!/bin/sh\n"; + script << "SCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n"; + script << "export LD_LIBRARY_PATH=\"$SCRIPT_DIR/lib:$LD_LIBRARY_PATH\"\n"; + script << "exec \"$SCRIPT_DIR/" << buildName << "\" \"$@\"\n"; + script.close(); + std::filesystem::permissions(launchScript, + std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, + std::filesystem::perm_options::add, ec); + } + } + + { + const std::filesystem::path desktopFile = exportRoot / (buildName + ".desktop"); + std::ofstream desktop(desktopFile); + if (desktop.is_open()) + { + desktop << "[Desktop Entry]\n"; + desktop << "Type=Application\n"; + desktop << "Name=" << runtimeSettings.GameName << "\n"; + desktop << "Exec=run-" << buildName << ".sh\n"; + if (!runtimeIconPath.empty()) + desktop << "Icon=" << (exportRoot / runtimeIconPath).string() << "\n"; + desktop << "Terminal=false\n"; + desktop << "Categories=Game;\n"; + } + } +#endif + LUX_CONSOLE_LOG_INFO("Runtime export complete: {}", exportRoot.string()); FileSystem::OpenDirectoryInExplorer(exportRoot); return true; diff --git a/Editor/Source/Panels/ProjectSettingsWindow.cpp b/Editor/Source/Panels/ProjectSettingsWindow.cpp index bdfa8614..4a210326 100644 --- a/Editor/Source/Panels/ProjectSettingsWindow.cpp +++ b/Editor/Source/Panels/ProjectSettingsWindow.cpp @@ -1,5 +1,6 @@ #include "lpch.h" #include "ProjectSettingsWindow.h" +#include "RuntimeExportUtils.h" #include "Lux/Asset/AssetManager.h" #include "Lux/ImGui/ImGuiEx.h" @@ -76,218 +77,19 @@ namespace Lux { } + using RuntimeExport::FileExists; + using RuntimeExport::FindRepositoryRootFrom; + using RuntimeExport::GetRuntimeExecutablePath; + using RuntimeExport::BuildRuntimeExecutable; + using RuntimeExport::ResolveScriptProjectFile; + using RuntimeExport::IsScriptModuleOutdated; + using RuntimeExport::BuildScriptModule; + constexpr RuntimeExportTarget s_RuntimeExportTargets[] = { RuntimeExportTarget::Debug, RuntimeExportTarget::Release, RuntimeExportTarget::Dist }; - - std::filesystem::path FindRepositoryRootFrom(std::filesystem::path start) - { - if (start.empty()) - return {}; - - std::error_code ec; - start = std::filesystem::absolute(start, ec).lexically_normal(); - if (ec) - return {}; - - if (std::filesystem::is_regular_file(start, ec)) - start = start.parent_path(); - - for (std::filesystem::path directory = start; !directory.empty(); directory = directory.parent_path()) - { - if (std::filesystem::exists(directory / "premake5.lua", ec) - && std::filesystem::exists(directory / "Core", ec) - && std::filesystem::exists(directory / "Lux-Runtime" / "premake5.lua", ec)) - { - return directory; - } - - if (directory == directory.root_path()) - break; - } - - return {}; - } - - std::filesystem::path GetRuntimeExecutablePath(RuntimeExportTarget target) - { - std::error_code ec; - const std::string runtimeOutputDirectory = std::string(RuntimeExportTargetToString(target)) + "-windows-x86_64"; - - std::vector candidates; - if (Ref activeProject = Project::GetActive()) - { - if (std::filesystem::path root = FindRepositoryRootFrom(activeProject->GetProjectDirectory()); !root.empty()) - candidates.emplace_back((root / "bin" / runtimeOutputDirectory / "Lux-Runtime" / "Lux-Runtime.exe").lexically_normal()); - } - - if (std::filesystem::path root = FindRepositoryRootFrom(std::filesystem::current_path(ec)); !root.empty()) - candidates.emplace_back((root / "bin" / runtimeOutputDirectory / "Lux-Runtime" / "Lux-Runtime.exe").lexically_normal()); - - for (const std::filesystem::path& candidate : candidates) - { - if (std::filesystem::exists(candidate, ec) && std::filesystem::is_regular_file(candidate, ec)) - return candidate; - } - - return {}; - } - - std::string QuotePowerShellArgument(std::string value) - { - std::string result = "'"; - for (char c : value) - { - if (c == '\'') - result += "''"; - else - result += c; - } - result += "'"; - return result; - } - - bool FileExists(const std::filesystem::path& path) - { - std::error_code ec; - return !path.empty() && std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); - } - - bool BuildRuntimeExecutable(RuntimeExportTarget target) - { - std::filesystem::path root = FindRepositoryRootFrom(Project::GetActiveProjectDirectory()); - if (root.empty()) - root = FindRepositoryRootFrom(std::filesystem::current_path()); - if (root.empty()) - { - LUX_CONSOLE_LOG_ERROR("Could not locate repository root for Lux-Runtime build."); - return false; - } - - const std::filesystem::path projectFile = root / "Lux-Runtime" / "Lux-Runtime.vcxproj"; - if (!std::filesystem::exists(projectFile)) - { - LUX_CONSOLE_LOG_ERROR("Lux-Runtime project file not found: {}", projectFile.string()); - return false; - } - - const std::filesystem::path msbuildPath = "C:/Program Files/Microsoft Visual Studio/18/Community/MSBuild/Current/Bin/MSBuild.exe"; - const std::string msbuild = std::filesystem::exists(msbuildPath) ? msbuildPath.string() : "MSBuild.exe"; - const std::string command = - "powershell -NoProfile -ExecutionPolicy Bypass -Command \"& " - + QuotePowerShellArgument(msbuild) + " " - + QuotePowerShellArgument(projectFile.string()) - + " /t:Build /p:Configuration=" + RuntimeExportTargetToString(target) - + " /p:Platform=x64 /m:1 /nr:false /v:minimal\""; - LUX_CONSOLE_LOG_INFO("Building Lux-Runtime ({})...", RuntimeExportTargetToString(target)); - const int result = std::system(command.c_str()); - if (result != 0) - { - LUX_CONSOLE_LOG_ERROR("Lux-Runtime build failed with exit code {}.", result); - return false; - } - - LUX_CONSOLE_LOG_INFO("Lux-Runtime build complete."); - return true; - } - - std::filesystem::path ResolveScriptProjectFile(Ref project) - { - if (!project) - return {}; - - std::filesystem::path scriptProject = project->GetScriptProjectPath(); - if (FileExists(scriptProject)) - return scriptProject; - - std::filesystem::path scriptProjectFilename = project->GetConfig().ScriptModulePath.filename(); - if (!scriptProjectFilename.empty()) - { - scriptProjectFilename.replace_extension(".csproj"); - scriptProject = project->GetAssetDirectory() / "Scripts" / scriptProjectFilename; - } - - return scriptProject; - } - - bool IsScriptModuleOutdated(const std::filesystem::path& scriptModule, const std::filesystem::path& scriptProject) - { - std::error_code ec; - if (!FileExists(scriptModule)) - return true; - if (!FileExists(scriptProject)) - return false; - - const auto moduleWriteTime = std::filesystem::last_write_time(scriptModule, ec); - if (ec) - return true; - - ec.clear(); - if (std::filesystem::last_write_time(scriptProject, ec) > moduleWriteTime && !ec) - return true; - - const std::filesystem::path scriptsDirectory = scriptProject.parent_path(); - if (!std::filesystem::exists(scriptsDirectory, ec)) - return false; - - for (const auto& entry : std::filesystem::recursive_directory_iterator(scriptsDirectory, ec)) - { - if (ec) - break; - if (!entry.is_regular_file(ec)) - continue; - - const std::filesystem::path relativePath = std::filesystem::relative(entry.path(), scriptsDirectory, ec); - if (!ec && !relativePath.empty()) - { - const std::filesystem::path first = *relativePath.begin(); - if (first == "Binaries" || first == "Intermediates") - continue; - } - - const std::filesystem::path extension = entry.path().extension(); - if (extension != ".cs" && extension != ".csproj" && extension != ".props" && extension != ".targets" && extension != ".lua") - continue; - - ec.clear(); - if (entry.last_write_time(ec) > moduleWriteTime && !ec) - return true; - } - - return false; - } - - bool BuildScriptModule(RuntimeExportTarget target) - { - Ref project = Project::GetActive(); - if (!project) - { - LUX_CONSOLE_LOG_ERROR("No active project to build scripts for."); - return false; - } - - const std::filesystem::path scriptProject = ResolveScriptProjectFile(project); - if (!FileExists(scriptProject)) - { - LUX_CONSOLE_LOG_ERROR("Script project file not found: {}", scriptProject.string()); - return false; - } - - if (!ScriptBuilder::BuildProject(scriptProject, RuntimeExportTargetToString(target))) - return false; - - const std::filesystem::path scriptModule = project->GetScriptModuleFilePath(); - if (!FileExists(scriptModule)) - { - LUX_CONSOLE_LOG_ERROR("Script build completed, but the script module was not found: {}", scriptModule.string()); - return false; - } - - LUX_CONSOLE_LOG_INFO("Script build complete: {}", scriptModule.string()); - return true; - } } ProjectSettingsWindow::ProjectSettingsWindow() diff --git a/Editor/Source/RuntimeExportUtils.cpp b/Editor/Source/RuntimeExportUtils.cpp new file mode 100644 index 00000000..8d2b32de --- /dev/null +++ b/Editor/Source/RuntimeExportUtils.cpp @@ -0,0 +1,380 @@ +#include "RuntimeExportUtils.h" + +#include "Lux/Core/Application.h" +#include "Lux/Core/Log.h" + +#include +#include +#include +#include + +namespace Lux::RuntimeExport { + + bool FileExists(const std::filesystem::path& path) + { + std::error_code ec; + return !path.empty() && std::filesystem::exists(path, ec) && std::filesystem::is_regular_file(path, ec); + } + + std::string SanitizeBuildName(std::string value) + { + if (value.empty()) + value = "LuxGame"; + + for (char& c : value) + { + const bool valid = std::isalnum((unsigned char)c) || c == '-' || c == '_'; + if (!valid) + c = '_'; + } + + return value; + } + + bool CopyFileIfExists(const std::filesystem::path& source, const std::filesystem::path& destination, bool required) + { + std::error_code ec; + if (!std::filesystem::exists(source, ec) || ec) + { + if (required) + LUX_CONSOLE_LOG_ERROR("Missing export file: {}", source.string()); + return false; + } + + std::filesystem::create_directories(destination.parent_path(), ec); + ec.clear(); + std::filesystem::copy_file(source, destination, std::filesystem::copy_options::overwrite_existing, ec); + if (ec) + { + LUX_CONSOLE_LOG_ERROR("Failed to copy '{}' to '{}': {}", source.string(), destination.string(), ec.message()); + return false; + } + + return true; + } + + bool CopyDirectoryRecursive(const std::filesystem::path& source, const std::filesystem::path& destination, bool skipDebugFiles) + { + std::error_code ec; + if (!std::filesystem::exists(source, ec) || ec) + return false; + + for (const auto& entry : std::filesystem::recursive_directory_iterator(source, ec)) + { + if (ec) + break; + + const std::filesystem::path relativePath = std::filesystem::relative(entry.path(), source, ec); + if (ec) + continue; + + if (!relativePath.empty() && *relativePath.begin() == "Cache") + continue; + + const std::filesystem::path target = destination / relativePath; + if (entry.is_directory(ec)) + { + std::filesystem::create_directories(target, ec); + continue; + } + + if (entry.is_regular_file(ec)) + { + if (skipDebugFiles) + { + const std::filesystem::path extension = entry.path().extension(); + if (extension == ".pdb" || extension == ".ilk" || extension == ".exp") + continue; + } + CopyFileIfExists(entry.path(), target); + } + } + + return true; + } + + std::filesystem::path FindFirstExistingDirectory(std::initializer_list candidates) + { + std::error_code ec; + for (const std::filesystem::path& candidate : candidates) + { + if (!candidate.empty() && std::filesystem::exists(candidate, ec) && std::filesystem::is_directory(candidate, ec)) + return candidate; + } + + return {}; + } + + std::filesystem::path FindRepositoryRootFrom(std::filesystem::path start) + { + if (start.empty()) + return {}; + + std::error_code ec; + start = std::filesystem::absolute(start, ec).lexically_normal(); + if (ec) + return {}; + + if (std::filesystem::is_regular_file(start, ec)) + start = start.parent_path(); + + for (std::filesystem::path directory = start; !directory.empty(); directory = directory.parent_path()) + { + if (std::filesystem::exists(directory / "premake5.lua", ec) + && std::filesystem::exists(directory / "Core", ec) + && std::filesystem::exists(directory / "Lux-Runtime" / "premake5.lua", ec)) + { + return directory; + } + + if (directory == directory.root_path()) + break; + } + + return {}; + } + + std::filesystem::path GetRuntimeOutputDirectory(RuntimeExportTarget target) + { + return std::string(RuntimeExportTargetToString(target)) + PlatformSuffix; + } + + bool IsBuildConfigurationDirectory(const std::filesystem::path& path) + { + const std::string directoryName = path.filename().string(); + return path.parent_path().filename() == "bin" && directoryName.find(PlatformSuffix) != std::string::npos; + } + + std::filesystem::path GetRuntimeExecutablePath(RuntimeExportTarget target) + { + std::error_code ec; + const std::filesystem::path current = std::filesystem::current_path(ec); + if (ec) + return {}; + + const std::filesystem::path runtimeOutputDirectory = GetRuntimeOutputDirectory(target); + std::vector candidates; + + if (std::filesystem::path root = FindRepositoryRootFrom(current); !root.empty()) + candidates.emplace_back((root / "bin" / runtimeOutputDirectory / "Lux-Runtime" / RuntimeExeName).lexically_normal()); + + if (Ref activeProject = Project::GetActive()) + { + if (std::filesystem::path root = FindRepositoryRootFrom(activeProject->GetProjectDirectory()); !root.empty()) + candidates.emplace_back((root / "bin" / runtimeOutputDirectory / "Lux-Runtime" / RuntimeExeName).lexically_normal()); + } + + const std::filesystem::path buildConfigDirectory = current.filename() == "Editor" ? current.parent_path() : current; + if (IsBuildConfigurationDirectory(buildConfigDirectory)) + candidates.emplace_back((buildConfigDirectory / "Lux-Runtime" / RuntimeExeName).lexically_normal()); + + for (const std::filesystem::path& candidate : candidates) + { + if (FileExists(candidate)) + return candidate; + } + + return {}; + } + + bool IsRuntimeExecutableOutdated(const std::filesystem::path& runtimeExe, const std::filesystem::path& repositoryRoot) + { + std::error_code ec; + if (runtimeExe.empty() || !std::filesystem::exists(runtimeExe, ec)) + return true; + + const auto executableWriteTime = std::filesystem::last_write_time(runtimeExe, ec); + if (ec) + return true; + + const std::array sourceRoots = { + repositoryRoot / "Lux-Runtime", + repositoryRoot / "Core" / "Source" + }; + + for (const auto& sourceRoot : sourceRoots) + { + if (!std::filesystem::exists(sourceRoot, ec)) + continue; + + for (const auto& entry : std::filesystem::recursive_directory_iterator(sourceRoot, ec)) + { + if (ec) + break; + if (!entry.is_regular_file(ec)) + continue; + + const std::filesystem::path extension = entry.path().extension(); + if (extension != ".cpp" && extension != ".h" && extension != ".hpp" && extension != ".c" && extension != ".rc" && extension != ".lua") + continue; + + if (entry.last_write_time(ec) > executableWriteTime && !ec) + return true; + } + } + + return false; + } + + bool BuildRuntimeExecutable(RuntimeExportTarget target) + { + std::filesystem::path repositoryRoot = FindRepositoryRootFrom(std::filesystem::current_path()); + if (repositoryRoot.empty()) + { + if (Ref activeProject = Project::GetActive()) + repositoryRoot = FindRepositoryRootFrom(activeProject->GetProjectDirectory()); + } + + if (repositoryRoot.empty()) + { + LUX_CONSOLE_LOG_ERROR("Could not locate repository root for Lux-Runtime build."); + return false; + } + +#ifdef LUX_PLATFORM_LINUX + std::string config = RuntimeExportTargetToString(target); + std::transform(config.begin(), config.end(), config.begin(), ::tolower); + const std::string command = + "make -C \"" + (repositoryRoot / "Lux-Runtime").string() + "\"" + + " -f Makefile config=" + config; +#else + const std::filesystem::path projectFile = repositoryRoot / "Lux-Runtime" / "Lux-Runtime.vcxproj"; + if (!FileExists(projectFile)) + { + LUX_CONSOLE_LOG_ERROR("Lux-Runtime project file not found: {}", projectFile.string()); + return false; + } + + auto QuotePowerShellArgument = [](std::string value) -> std::string + { + std::string result = "'"; + for (char c : value) + { + if (c == '\'') + result += "''"; + else + result += c; + } + result += "'"; + return result; + }; + + const std::filesystem::path msbuildPath = "C:/Program Files/Microsoft Visual Studio/18/Community/MSBuild/Current/Bin/MSBuild.exe"; + const std::string msbuild = FileExists(msbuildPath) ? msbuildPath.string() : "MSBuild.exe"; + const std::string command = + "powershell -NoProfile -ExecutionPolicy Bypass -Command \"& " + + QuotePowerShellArgument(msbuild) + " " + + QuotePowerShellArgument(projectFile.string()) + + " /t:Build /p:Configuration=" + RuntimeExportTargetToString(target) + + " /p:Platform=x64 /m:1 /nr:false /v:minimal\""; +#endif + + LUX_CONSOLE_LOG_INFO("Building Lux-Runtime ({})...", RuntimeExportTargetToString(target)); + const int result = std::system(command.c_str()); + if (result != 0) + { + LUX_CONSOLE_LOG_ERROR("Lux-Runtime build failed with exit code {}.", result); + return false; + } + + LUX_CONSOLE_LOG_INFO("Lux-Runtime build complete."); + return true; + } + + std::filesystem::path ResolveScriptProjectFile(Ref project) + { + if (!project) + return {}; + + std::filesystem::path scriptProject = project->GetScriptProjectPath(); + if (FileExists(scriptProject)) + return scriptProject; + + std::filesystem::path scriptProjectFilename = project->GetConfig().ScriptModulePath.filename(); + if (!scriptProjectFilename.empty()) + { + scriptProjectFilename.replace_extension(".csproj"); + scriptProject = project->GetAssetDirectory() / "Scripts" / scriptProjectFilename; + } + + return scriptProject; + } + + bool IsScriptModuleOutdated(const std::filesystem::path& scriptModule, const std::filesystem::path& scriptProject) + { + std::error_code ec; + if (!FileExists(scriptModule)) + return true; + if (!FileExists(scriptProject)) + return false; + + const auto moduleWriteTime = std::filesystem::last_write_time(scriptModule, ec); + if (ec) + return true; + + ec.clear(); + if (std::filesystem::last_write_time(scriptProject, ec) > moduleWriteTime && !ec) + return true; + + const std::filesystem::path scriptsDirectory = scriptProject.parent_path(); + if (!std::filesystem::exists(scriptsDirectory, ec)) + return false; + + for (const auto& entry : std::filesystem::recursive_directory_iterator(scriptsDirectory, ec)) + { + if (ec) + break; + if (!entry.is_regular_file(ec)) + continue; + + const std::filesystem::path relativePath = std::filesystem::relative(entry.path(), scriptsDirectory, ec); + if (!ec && !relativePath.empty()) + { + const std::filesystem::path first = *relativePath.begin(); + if (first == "Binaries" || first == "Intermediates") + continue; + } + + const std::filesystem::path extension = entry.path().extension(); + if (extension != ".cs" && extension != ".csproj" && extension != ".props" && extension != ".targets" && extension != ".lua") + continue; + + ec.clear(); + if (entry.last_write_time(ec) > moduleWriteTime && !ec) + return true; + } + + return false; + } + + bool BuildScriptModule(RuntimeExportTarget target) + { + Ref project = Project::GetActive(); + if (!project) + { + LUX_CONSOLE_LOG_ERROR("No active project to build scripts for."); + return false; + } + + const std::filesystem::path scriptProject = ResolveScriptProjectFile(project); + if (!FileExists(scriptProject)) + { + LUX_CONSOLE_LOG_ERROR("Script project file not found: {}", scriptProject.string()); + return false; + } + + if (!ScriptBuilder::BuildProject(scriptProject, RuntimeExportTargetToString(target))) + return false; + + const std::filesystem::path scriptModule = project->GetScriptModuleFilePath(); + if (!FileExists(scriptModule)) + { + LUX_CONSOLE_LOG_ERROR("Script build completed, but the script module was not found: {}", scriptModule.string()); + return false; + } + + LUX_CONSOLE_LOG_INFO("Script build complete: {}", scriptModule.string()); + return true; + } + +} diff --git a/Editor/Source/RuntimeExportUtils.h b/Editor/Source/RuntimeExportUtils.h new file mode 100644 index 00000000..19151fc6 --- /dev/null +++ b/Editor/Source/RuntimeExportUtils.h @@ -0,0 +1,55 @@ +#pragma once + +#include "Lux/Project/Project.h" +#include "Lux/Scripting/ScriptBuilder.h" + +#include +#include +#include + +namespace Lux::RuntimeExport { + + inline constexpr const char* RuntimeProjectFile = "Project.luxruntime"; + inline constexpr const char* RuntimeAssetPackFile = "AssetPack.lap"; + inline constexpr const char* RuntimeShaderPackFile = "ShaderPack.lsp"; + + inline constexpr const char* PlatformSuffix = +#ifdef LUX_PLATFORM_LINUX + "-linux-x86_64"; +#else + "-windows-x86_64"; +#endif + + inline constexpr const char* PlatformExportLabel = +#ifdef LUX_PLATFORM_LINUX + "-Linux-x86_64"; +#else + "-Windows-x86_64"; +#endif + + inline constexpr const char* RuntimeExeName = +#ifdef LUX_PLATFORM_LINUX + "Lux-Runtime"; +#else + "Lux-Runtime.exe"; +#endif + + bool FileExists(const std::filesystem::path& path); + std::string SanitizeBuildName(std::string value); + + bool CopyFileIfExists(const std::filesystem::path& source, const std::filesystem::path& destination, bool required = false); + bool CopyDirectoryRecursive(const std::filesystem::path& source, const std::filesystem::path& destination, bool skipDebugFiles = false); + std::filesystem::path FindFirstExistingDirectory(std::initializer_list candidates); + + std::filesystem::path FindRepositoryRootFrom(std::filesystem::path start); + std::filesystem::path GetRuntimeOutputDirectory(RuntimeExportTarget target); + bool IsBuildConfigurationDirectory(const std::filesystem::path& path); + std::filesystem::path GetRuntimeExecutablePath(RuntimeExportTarget target); + bool IsRuntimeExecutableOutdated(const std::filesystem::path& runtimeExe, const std::filesystem::path& repositoryRoot); + bool BuildRuntimeExecutable(RuntimeExportTarget target); + + std::filesystem::path ResolveScriptProjectFile(Ref project); + bool IsScriptModuleOutdated(const std::filesystem::path& scriptModule, const std::filesystem::path& scriptProject); + bool BuildScriptModule(RuntimeExportTarget target); + +} diff --git a/Editor/premake5.lua b/Editor/premake5.lua index 239ed942..7c580d0c 100644 --- a/Editor/premake5.lua +++ b/Editor/premake5.lua @@ -60,7 +60,7 @@ project "Editor" filter "system:linux" defines { "LUX_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } links { "dw", "dl", "unwind", "pthread", "X11" } - linkoptions { "-Wl,--start-group" } + linkoptions { "-Wl,--start-group", "-Wl,-rpath,'$$ORIGIN/lib'" } -- Link nethost for Coral .NET hosting if os.host() == "linux" then diff --git a/Lux-Runtime/premake5.lua b/Lux-Runtime/premake5.lua index 5852de5d..fb8aee30 100644 --- a/Lux-Runtime/premake5.lua +++ b/Lux-Runtime/premake5.lua @@ -68,10 +68,26 @@ project "Lux-Runtime" filter "system:linux" defines { "LUX_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } - links { "dw", "dl", "unwind", "pthread" } + links { "dw", "dl", "unwind", "pthread", "X11" } + linkoptions { "-Wl,--start-group", "-Wl,-rpath,'$$ORIGIN/lib'" } if gtkLinkOptions then linkoptions { gtkLinkOptions } end + if os.host() == "linux" then + LinkNethost() + end + + filter { "system:linux", "configurations:Debug or configurations:Debug-AS" } + postbuildcommands { + '{COPYDIR} "../Editor/Resources" "%{cfg.targetdir}/Resources"', + '{COPYDIR} "../Editor/DotNet" "%{cfg.targetdir}/DotNet"', + } + + filter { "system:linux", "configurations:Release or configurations:Dist" } + postbuildcommands { + '{COPYDIR} "../Editor/Resources" "%{cfg.targetdir}/Resources"', + '{COPYDIR} "../Editor/DotNet" "%{cfg.targetdir}/DotNet"', + } filter "configurations:Debug or configurations:Debug-AS" symbols "On" diff --git a/ScriptCore/premake5.lua b/ScriptCore/premake5.lua index cc807f5b..b16a4718 100644 --- a/ScriptCore/premake5.lua +++ b/ScriptCore/premake5.lua @@ -1,6 +1,9 @@ -- ScriptCore is a premake-generated C# project built inside Lux.sln, mirroring Hazel-ScriptCore. -- It links Coral.Managed (the C# host assembly) and lands in Editor/Resources/Scripts, where -- ScriptEngine loads the core assembly from. +-- On Linux, ScriptCore is built via `dotnet build` in scripts/Linux-Build.sh instead of gmake. +if os.host() == "linux" then return end + project "ScriptCore" kind "SharedLib" language "C#" diff --git a/packaging/linux/build-appimage.sh b/packaging/linux/build-appimage.sh new file mode 100755 index 00000000..b52a4a7a --- /dev/null +++ b/packaging/linux/build-appimage.sh @@ -0,0 +1,133 @@ +#!/bin/sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +CONFIG="${1:-Release}" +case "$CONFIG" in + debug|Debug) CONFIG=Debug ;; + release|Release) CONFIG=Release ;; + dist|Dist) CONFIG=Dist ;; + *) + echo "Unknown config: $CONFIG" + echo "Usage: $0 [debug|release|dist] [editor|runtime]" + exit 1 + ;; +esac + +TARGET="${2:-editor}" +case "$TARGET" in + editor|Editor) TARGET=editor ;; + runtime|Runtime) TARGET=runtime ;; + *) + echo "Unknown target: $TARGET" + echo "Usage: $0 [debug|release|dist] [editor|runtime]" + exit 1 + ;; +esac + +CONFIG_LOWER=$(echo "$CONFIG" | tr '[:upper:]' '[:lower:]') +BIN_DIR="$REPO_ROOT/bin/$CONFIG-linux-x86_64" + +if [ "$TARGET" = "editor" ]; then + APP_NAME="LuxEditor" + SOURCE_BIN="$BIN_DIR/Editor/Editor" + DESKTOP_FILE="$SCRIPT_DIR/lux-editor.desktop" +else + APP_NAME="LuxRuntime" + SOURCE_BIN="$BIN_DIR/Lux-Runtime/Lux-Runtime" + DESKTOP_FILE="" # generated below +fi + +if [ ! -f "$SOURCE_BIN" ]; then + echo "Binary not found: $SOURCE_BIN" + echo "Build with: make config=$CONFIG_LOWER $( [ "$TARGET" = "editor" ] && echo "Editor" || echo "Lux-Runtime" )" + exit 1 +fi + +APPDIR="$REPO_ROOT/packaging/linux/$APP_NAME.AppDir" +rm -rf "$APPDIR" +mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps" + +cp "$SOURCE_BIN" "$APPDIR/usr/bin/" + +VULKAN_SDK="$REPO_ROOT/Core/vendor/VulkanSDK/x86_64" +for lib in "$VULKAN_SDK/lib/"*.so*; do + [ -f "$lib" ] && cp -P "$lib" "$APPDIR/usr/lib/" +done + +for lib in "$REPO_ROOT/Core/vendor/assimp/bin/linux/"*.so*; do + [ -f "$lib" ] && cp -P "$lib" "$APPDIR/usr/lib/" +done + +if [ -d "$REPO_ROOT/Core/vendor/NvidiaAftermath/lib/x64/linux" ]; then + for lib in "$REPO_ROOT/Core/vendor/NvidiaAftermath/lib/x64/linux/"*.so*; do + [ -f "$lib" ] && cp -P "$lib" "$APPDIR/usr/lib/" + done +fi + +if [ "$TARGET" = "editor" ]; then + cp -r "$BIN_DIR/Editor/Resources" "$APPDIR/usr/bin/Resources" 2>/dev/null || \ + cp -r "$REPO_ROOT/Editor/Resources" "$APPDIR/usr/bin/Resources" + [ -d "$BIN_DIR/Editor/DotNet" ] && cp -r "$BIN_DIR/Editor/DotNet" "$APPDIR/usr/bin/DotNet" + [ -d "$REPO_ROOT/Editor/DotNet" ] && [ ! -d "$APPDIR/usr/bin/DotNet" ] && \ + cp -r "$REPO_ROOT/Editor/DotNet" "$APPDIR/usr/bin/DotNet" +else + cp -r "$BIN_DIR/Lux-Runtime/Resources" "$APPDIR/usr/bin/Resources" 2>/dev/null || \ + cp -r "$REPO_ROOT/Editor/Resources" "$APPDIR/usr/bin/Resources" + [ -d "$BIN_DIR/Lux-Runtime/DotNet" ] && cp -r "$BIN_DIR/Lux-Runtime/DotNet" "$APPDIR/usr/bin/DotNet" + [ -d "$REPO_ROOT/Editor/DotNet" ] && [ ! -d "$APPDIR/usr/bin/DotNet" ] && \ + cp -r "$REPO_ROOT/Editor/DotNet" "$APPDIR/usr/bin/DotNet" +fi + +if [ -d "$VULKAN_SDK/bin" ]; then + cp "$VULKAN_SDK/bin/dxc" "$APPDIR/usr/bin/" 2>/dev/null || true + cp "$VULKAN_SDK/bin/dxc-"* "$APPDIR/usr/bin/" 2>/dev/null || true +fi + +if [ "$TARGET" = "editor" ]; then + EXEC_NAME="Editor" + cp "$DESKTOP_FILE" "$APPDIR/usr/share/applications/" +else + EXEC_NAME="Lux-Runtime" + cat > "$APPDIR/usr/share/applications/lux-runtime.desktop" << 'DESKTOP' +[Desktop Entry] +Type=Application +Name=Lux Runtime +Comment=Lux Engine Runtime Player +Exec=Lux-Runtime +Icon=lux-runtime +Terminal=false +Categories=Game; +DESKTOP +fi + +# Placeholder icon (1x1 PNG) +if [ ! -f "$APPDIR/usr/share/icons/hicolor/256x256/apps/lux-${TARGET}.png" ]; then + printf '\x89PNG\r\n\x1a\n' > "$APPDIR/usr/share/icons/hicolor/256x256/apps/lux-${TARGET}.png" +fi + +cat > "$APPDIR/AppRun" << APPRUN +#!/bin/sh +SELF="\$(readlink -f "\$0")" +HERE="\${SELF%/*}" +export LD_LIBRARY_PATH="\$HERE/usr/lib:\$LD_LIBRARY_PATH" +export PATH="\$HERE/usr/bin:\$PATH" +exec "\$HERE/usr/bin/$EXEC_NAME" "\$@" +APPRUN +chmod +x "$APPDIR/AppRun" + +ln -sf "usr/share/applications/lux-${TARGET}.desktop" "$APPDIR/lux-${TARGET}.desktop" +ln -sf "usr/share/icons/hicolor/256x256/apps/lux-${TARGET}.png" "$APPDIR/.DirIcon" + +APPIMAGETOOL="$(command -v appimagetool 2>/dev/null || echo "")" +if [ -z "$APPIMAGETOOL" ]; then + echo "appimagetool not found. AppDir created at: $APPDIR" + echo "Install appimagetool and run: appimagetool \"$APPDIR\"" + exit 0 +fi + +OUTPUT="$REPO_ROOT/packaging/linux/$APP_NAME-x86_64.AppImage" +"$APPIMAGETOOL" "$APPDIR" "$OUTPUT" +echo "AppImage created: $OUTPUT" diff --git a/packaging/linux/lux-editor.desktop b/packaging/linux/lux-editor.desktop new file mode 100644 index 00000000..721074fb --- /dev/null +++ b/packaging/linux/lux-editor.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Lux Editor +Comment=Lux Engine Editor +Exec=run-lux-editor.sh +Icon=lux-editor +Terminal=false +Categories=Development;IDE; +MimeType=application/x-luxproject; diff --git a/scripts/Linux-Build.sh b/scripts/Linux-Build.sh index c838b02d..52c42977 100644 --- a/scripts/Linux-Build.sh +++ b/scripts/Linux-Build.sh @@ -69,3 +69,4 @@ fi make config=$CONFIG Dependencies Dependencies/Renderer "$@" make -C Core -f Makefile config=$CONFIG make -C Editor -f Makefile config=$CONFIG + make -C Lux-Runtime -f Makefile config=$CONFIG diff --git a/scripts/Linux-RunRuntime.sh b/scripts/Linux-RunRuntime.sh new file mode 100755 index 00000000..091ea7ca --- /dev/null +++ b/scripts/Linux-RunRuntime.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +export LUX_DIR=$(realpath .) +if [ -n "${BUILD_CONFIG+set}" ] + then + true + elif [ -n "$1" ] + then + case "$1" in + debug|Debug) export BUILD_CONFIG=Debug ;; + release|Release) export BUILD_CONFIG=Release ;; + dist|Dist) export BUILD_CONFIG=Dist ;; + *) + echo "Unknown config: $1" + echo "Usage: $0 [debug|release|dist]" + exit 1 + ;; + esac + shift + else + echo "Select build configuration:" + echo " 1) Debug" + echo " 2) Release" + echo " 3) Dist" + printf "Choice [1-3]: " + read choice + case "$choice" in + 1) export BUILD_CONFIG=Debug ;; + 2) export BUILD_CONFIG=Release ;; + 3) export BUILD_CONFIG=Dist ;; + *) + echo "Invalid choice" + exit 1 + ;; + esac +fi +export VULKAN_SDK=$(realpath Core/vendor/VulkanSDK/x86_64) +export VK_LAYER_PATH="$VULKAN_SDK/share/vulkan/explicit_layer.d" +export PATH="$VULKAN_SDK/bin:$PATH" +export LD_LIBRARY_PATH="$VULKAN_SDK/lib:$LUX_DIR/Core/vendor/assimp/bin/linux:$LUX_DIR/Core/vendor/NvidiaAftermath/lib/x64/linux" + +RUNTIME_DIR="$LUX_DIR/bin/$BUILD_CONFIG-linux-x86_64/Lux-Runtime" +cd "$RUNTIME_DIR" +"$RUNTIME_DIR/Lux-Runtime" "$@" From 2b111b9c22fe0f6b8e38924b1b28196ac274a32d Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 17:48:12 -0400 Subject: [PATCH 11/20] Fix Linux scripting, fullscreen swap chain extent, and script project builds Scripting was silently dead in exported runtimes: the app assembly is loaded from the asset pack via LoadAssemblyFromMemory, but assemblies loaded from a byte[] have an empty Assembly.Location, so Coral enumerated zero local types. BuildAssemblyCache then cached nothing, IsValidScript() failed, and the entity was dropped in OnRuntimeStart and again every frame in OnUpdateRuntime - both without any diagnostic. Stage the packed binary into the shadow directory and load it by path (the route the editor already proves works), keeping the in-memory load as a warned fallback. Coral is a submodule and was not touched. - Fix ScriptGlue component registration on Linux: typeid().name() returns the Itanium-mangled "N3Lux18TransformComponentE" rather than MSVC's readable "struct Lux::TransformComponent", so the namespace strip kept the whole mangled string and no component ever resolved. Demangle via __cxa_demangle; the Windows path is unchanged. - Report scripts that fail to register instead of skipping them silently, and log cached type / registered script counts per assembly. - Add SDK-style LuxSample.csproj + Directory.Build.props so the sample script project builds via `dotnet build` on Linux (premake's C# generator needs csc). References use Private=false: copying ScriptCore/Coral.Managed next to the game assembly resolves a second ScriptCore, giving a distinct Lux.Entity identity that fails IsSubclassOf. BaseIntermediateOutputPath lives in Directory.Build.props so NuGet stops emitting an obj/ that made the editor consider scripts perpetually stale. - Un-ignore the two hand-authored csproj files. The blanket **.csproj rule dates from when they were all premake-generated, but Linux-Build.sh builds ScriptCore.csproj by path, so a fresh clone could not build at all. - Fix fullscreen swap chain creation: only the windowed branch reconciled the window size against the real framebuffer, and Create() was passed the requested spec size rather than the actual one. Also clamp imageExtent to [minImageExtent, maxImageExtent] (VUID-01274) - violating it is reported as VK_ERROR_OUT_OF_DEVICE_MEMORY - and stop assigning the 0xFFFFFFFF sentinel into m_Width/m_Height on the resize path. - Ignore exported standalone game builds. Co-Authored-By: Claude Opus 5 --- .gitignore | 8 +++ Core/Source/Lux/Core/Window.cpp | 7 ++- .../Lux/Platform/Vulkan/VulkanSwapChain.cpp | 17 +++++- Core/Source/Lux/Scene/Scene.cpp | 7 +++ Core/Source/Lux/Scripting/ScriptEngine.cpp | 55 ++++++++++++++++++- Core/Source/Lux/Scripting/ScriptGlue.cpp | 32 ++++++++++- .../Assets/Scenes/NewSceneSystem.luxscene | 2 +- .../Assets/Scripts/Directory.Build.props | 9 +++ .../Assets/Scripts/LuxSample.csproj | 45 +++++++++++++++ Editor/LuxSampleProject/LuxSample.luxproj | 8 +-- ScriptCore/ScriptCore.csproj | 18 ++++++ 11 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props create mode 100644 Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj create mode 100644 ScriptCore/ScriptCore.csproj diff --git a/.gitignore b/.gitignore index 1cf5d46e..cdf606a6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,11 @@ premake5 **.csproj **.csproj.user +# ...except the hand-authored SDK-style projects. These are sources, not premake output: +# the Linux build drives them with `dotnet build` (premake's C# generator needs csc). +!ScriptCore/ScriptCore.csproj +!Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj + # Directories scripts/__pycache__ @@ -52,6 +57,9 @@ Core/vendor/tracy Editor/LuxSampleProject/Assets/Scripts/Binaries ScriptCore/obj/ Editor/LuxSampleProject/Assets/Scripts/obj/ +# Exported standalone game builds (produced by the editor's Export Runtime) +Editor/LuxSampleProject/*-Linux-x86_64/ +Editor/LuxSampleProject/*-Windows-x86_64/ Editor/DotNet/ Editor/Resources/Cache Lux.slnx diff --git a/Core/Source/Lux/Core/Window.cpp b/Core/Source/Lux/Core/Window.cpp index 88f7b518..32c8009d 100644 --- a/Core/Source/Lux/Core/Window.cpp +++ b/Core/Source/Lux/Core/Window.cpp @@ -211,7 +211,10 @@ namespace Lux { glfwSetWindowMonitor(m_WindowHandle, glfwGetPrimaryMonitor(), 0, 0, m_Specification.Width, m_Specification.Height, deviceParams.refreshRate); } - else + + // The compositor/monitor decides the final surface size — in fullscreen it is the + // monitor mode, not the requested size. Always reconcile against the real framebuffer, + // otherwise the swap chain is created with an extent the surface doesn't allow. { int fbWidth = 0, fbHeight = 0; glfwGetFramebufferSize(m_WindowHandle, &fbWidth, &fbHeight); @@ -295,7 +298,7 @@ namespace Lux { m_DeviceManager->InitSurfaceCapabilities(*(uint64_t*)&m_WindowSurface); m_SwapChain = lnew VulkanSwapChain(m_WindowSurface); - m_SwapChain->Create(m_Specification.Width, m_Specification.Height); + m_SwapChain->Create(m_Data.Width, m_Data.Height); #if OLD // Create Renderer Context diff --git a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp index 4c017491..7b314dab 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp @@ -50,12 +50,20 @@ namespace Lux { return false; } + // currentExtent == 0xFFFFFFFF means "the surface size is whatever the swap chain + // asks for" (typical on Wayland). Otherwise the surface dictates the size and the + // requested one must be ignored. if (surfaceCaps.currentExtent.width != 0xFFFFFFFF) { m_Width = surfaceCaps.currentExtent.width; m_Height = surfaceCaps.currentExtent.height; } + // imageExtent must lie within [minImageExtent, maxImageExtent] (VUID-01274). + // Violating this is reported as VK_ERROR_OUT_OF_DEVICE_MEMORY by some drivers. + m_Width = std::clamp(m_Width, surfaceCaps.minImageExtent.width, surfaceCaps.maxImageExtent.width); + m_Height = std::clamp(m_Height, surfaceCaps.minImageExtent.height, surfaceCaps.maxImageExtent.height); + if (m_Width == 0 || m_Height == 0) { LUX_CORE_WARN("VulkanSwapChain::Create - surface extent is 0x0, deferring swap chain creation."); @@ -311,8 +319,13 @@ namespace Lux { return false; } - m_Width = surfaceCaps.currentExtent.width; - m_Height = surfaceCaps.currentExtent.height; + // 0xFFFFFFFF means the surface takes its size from the swap chain + // (Wayland); keep the current extent and let Create() clamp it. + if (surfaceCaps.currentExtent.width != 0xFFFFFFFF) + { + m_Width = surfaceCaps.currentExtent.width; + m_Height = surfaceCaps.currentExtent.height; + } if (m_Width == 0 || m_Height == 0) return false; diff --git a/Core/Source/Lux/Scene/Scene.cpp b/Core/Source/Lux/Scene/Scene.cpp index 8f72452c..9898d746 100644 --- a/Core/Source/Lux/Scene/Scene.cpp +++ b/Core/Source/Lux/Scene/Scene.cpp @@ -460,7 +460,14 @@ namespace Lux { Entity entity = { e, this }; const auto& sc = entity.GetComponent(); if (!scriptEngine.IsValidScript(sc.ScriptID)) + { + // Without this the entity is skipped here AND every frame in OnUpdateRuntime + // (which drops entities with no instance), so the script silently never runs. + LUX_CORE_ERROR("[Scripting] Script '{}' (ID {}) is not present in the loaded app assembly - it will not run. " + "Rebuild the script project and re-export so the assembly matches the scene.", + sc.ClassName.empty() ? "" : sc.ClassName, (uint64_t)sc.ScriptID); continue; + } UUID entityID = entity.GetUUID(); if (!m_ScriptStorage.EntityStorage.contains(entityID)) diff --git a/Core/Source/Lux/Scripting/ScriptEngine.cpp b/Core/Source/Lux/Scripting/ScriptEngine.cpp index 8be55bea..9a89c037 100644 --- a/Core/Source/Lux/Scripting/ScriptEngine.cpp +++ b/Core/Source/Lux/Scripting/ScriptEngine.cpp @@ -218,9 +218,41 @@ namespace Lux { void ScriptEngine::LoadProjectAssemblyRuntime(Buffer data) { m_AppAssemblyData.reset(); - m_AppAssemblyData = CreateScope(); - m_AppAssemblyData->Assembly = &m_LoadContext->LoadAssemblyFromMemory(reinterpret_cast(data.Data), (int64_t)data.Size); + + // An assembly loaded from a byte[] has an empty Assembly.Location, and Coral enumerates + // local types by location - so LoadAssemblyFromMemory yields zero types, BuildAssemblyCache + // caches nothing, and every script silently fails to register. Stage the packed binary on + // disk and use the same path-based load the editor uses (which does report its types). + std::error_code ec; + if (m_ShadowDirRoot.empty()) + m_ShadowDirRoot = std::filesystem::temp_directory_path() / "LuxScriptShadow"; + + const std::filesystem::path stagedDir = m_ShadowDirRoot / std::to_string(m_ShadowCounter++); + std::filesystem::create_directories(stagedDir, ec); + + bool staged = false; + const std::filesystem::path stagedAssembly = stagedDir / "App.dll"; + if (!ec) + { + std::ofstream out(stagedAssembly, std::ios::binary | std::ios::trunc); + if (out.is_open()) + { + out.write(reinterpret_cast(data.Data), (std::streamsize)data.Size); + staged = out.good(); + } + } + + if (staged) + { + m_AppAssemblyData->Assembly = &m_LoadContext->LoadAssembly(stagedAssembly.string()); + } + else + { + LUX_CORE_WARN("[Scripting] Could not stage the packed app assembly to disk; falling back to an in-memory load, " + "which may not expose any script types."); + m_AppAssemblyData->Assembly = &m_LoadContext->LoadAssemblyFromMemory(reinterpret_cast(data.Data), (int64_t)data.Size); + } if (m_AppAssemblyData->Assembly->GetLoadStatus() != Coral::AssemblyLoadStatus::Success) { @@ -313,6 +345,10 @@ namespace Lux { // the assembly being cached (the app assembly has no local Lux.Entity). Both share one // ALC, so cross-assembly IsSubclassOf works. Coral::Type& entityType = m_CoreAssemblyData->Assembly->GetLocalType("Lux.Entity"); + if (!entityType) + LUX_CORE_ERROR("[Scripting] Could not resolve 'Lux.Entity' in ScriptCore - no script will be registered."); + + uint32_t scriptCount = 0; for (const Coral::Type& constType : types) { @@ -325,7 +361,19 @@ namespace Lux { assemblyData->CachedTypes[scriptID] = &type; if (!entityType || !type.IsSubclassOf(entityType)) + { + // Only interesting for the app assembly - ScriptCore is nearly all non-script types. + // A game type landing here usually means a duplicate ScriptCore was resolved, giving + // two distinct Lux.Entity identities; the script would otherwise vanish silently. + if (assemblyData != m_CoreAssemblyData.get()) + { + LUX_CORE_TRACE("[Scripting] Type '{}' (ID {}) is not a Lux.Entity subclass; not registered as a script.", + fullName, (uint64_t)scriptID); + } continue; + } + + scriptCount++; auto& metadata = m_ScriptMetadata[scriptID]; metadata.FullName = fullName; @@ -402,6 +450,9 @@ namespace Lux { temp.Destroy(); } + + LUX_CORE_INFO("[Scripting] Cached {} type(s), {} registered as scripts (Lux.Entity subclasses).", + types.size(), scriptCount); } } diff --git a/Core/Source/Lux/Scripting/ScriptGlue.cpp b/Core/Source/Lux/Scripting/ScriptGlue.cpp index 8a56f5bf..913489d0 100644 --- a/Core/Source/Lux/Scripting/ScriptGlue.cpp +++ b/Core/Source/Lux/Scripting/ScriptGlue.cpp @@ -27,6 +27,11 @@ #include +#if !defined(LUX_PLATFORM_WINDOWS) +#include +#include +#endif + namespace Lux { // Component dispatch maps, keyed by Coral::TypeId (== managed typeof(T) cache id). @@ -580,12 +585,35 @@ namespace Lux { #pragma endregion + // typeid().name() is implementation-defined. MSVC returns a readable + // "struct Lux::TransformComponent", while the Itanium ABI (GCC/Clang) returns the mangled + // "N3Lux18TransformComponentE" - which contains no ':', so the namespace strip below would + // otherwise keep the whole mangled string and no component would ever resolve. + static std::string DemangleTypeName(const char* name) + { +#if defined(LUX_PLATFORM_WINDOWS) + return name; +#else + int status = 0; + char* demangled = abi::__cxa_demangle(name, nullptr, nullptr, &status); + if (status != 0 || demangled == nullptr) + { + std::free(demangled); + return name; + } + + std::string result = demangled; + std::free(demangled); + return result; +#endif + } + template static void RegisterManagedComponent(Coral::ManagedAssembly& coreAssembly) { - std::string_view typeName = typeid(TComponent).name(); + const std::string typeName = DemangleTypeName(typeid(TComponent).name()); size_t pos = typeName.find_last_of(':'); - std::string_view structName = typeName.substr(pos + 1); + std::string_view structName = std::string_view(typeName).substr(pos + 1); std::string managedTypename = std::format("Lux.{}", structName); Coral::Type& managedType = coreAssembly.GetLocalType(managedTypename); diff --git a/Editor/LuxSampleProject/Assets/Scenes/NewSceneSystem.luxscene b/Editor/LuxSampleProject/Assets/Scenes/NewSceneSystem.luxscene index 8b06912a..1ea18608 100644 --- a/Editor/LuxSampleProject/Assets/Scenes/NewSceneSystem.luxscene +++ b/Editor/LuxSampleProject/Assets/Scenes/NewSceneSystem.luxscene @@ -22,7 +22,7 @@ Entities: Rotation: [0, 0, 0] Scale: [1, 1, 1] SkyLightComponent: - EnvironmentMap: 16912336775507277160 + EnvironmentMap: 17181252949308778436 Intensity: 0.239999995 Lod: 0 DynamicSky: false diff --git a/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props b/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props new file mode 100644 index 00000000..24fcb613 --- /dev/null +++ b/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props @@ -0,0 +1,9 @@ + + + + Intermediates/ + + diff --git a/Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj b/Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj new file mode 100644 index 00000000..631a52e0 --- /dev/null +++ b/Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj @@ -0,0 +1,45 @@ + + + + Library + net9.0 + LuxSample + LuxSample + true + disable + false + true + Binaries + + + + + + false + + + + + + + + + + + ../../../../Editor/Resources/Scripts/ScriptCore.dll + false + + + ../../../../Editor/DotNet/Coral.Managed.dll + false + + + diff --git a/Editor/LuxSampleProject/LuxSample.luxproj b/Editor/LuxSampleProject/LuxSample.luxproj index fb71bdb7..1b4301d1 100644 --- a/Editor/LuxSampleProject/LuxSample.luxproj +++ b/Editor/LuxSampleProject/LuxSample.luxproj @@ -14,10 +14,10 @@ Project: AutoSaveInterval: 300 RuntimeExport: GameName: LuxSample - WindowWidth: 3840 - WindowHeight: 2160 - Fullscreen: true - VSync: true + WindowWidth: 1920 + WindowHeight: 1080 + Fullscreen: false + VSync: false IconPath: Textures/luxLogo.png IconHandle: 12258613209527478073 TargetConfig: Release diff --git a/ScriptCore/ScriptCore.csproj b/ScriptCore/ScriptCore.csproj new file mode 100644 index 00000000..fcd2220a --- /dev/null +++ b/ScriptCore/ScriptCore.csproj @@ -0,0 +1,18 @@ + + + Library + net9.0 + true + enable + false + true + ../Editor/Resources/Scripts + ../Editor/Resources/Scripts/Intermediates/ + + + + + ../Editor/DotNet/Coral.Managed.dll + + + From cebae22a46cf655b5fc9d10f52adc6fc87ef9e87 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 18:24:30 -0400 Subject: [PATCH 12/20] One-shot Linux build script, generated csproj files, and CI NuGet restore Fix the Windows CI failure (NETSDK1004: project.assets.json not found). ScriptCore and Coral.Managed are premake-generated SDK-style C# projects inside Lux.sln, and those cannot build until a NuGet restore produces obj/project.assets.json. Nothing in the pipeline ran one. Pass /restore to MSBuild so the Restore target runs first with the same properties as the build. This break is pre-existing on dev (same error since "Merge Coral (.NET 9) scripting into dev"), not introduced by the Linux port. - scripts/Linux-Build.sh is now a single entry point: prerequisite checks, submodule sync, C# project generation, managed assemblies, native engine, and the sample game's scripts. Also fixes it only working from the repo root (LUX_DIR came from `realpath .`) and builds with -j$(nproc), overridable via JOBS. - Make the ScriptCore premake guard action-aware: skip only for the gmake actions, whose C# generator shells out to csc. vs2022 is wanted on Linux because it emits SDK-style .csproj files that `dotnet build` consumes on any platform. Requires `premake5 --os=linux vs2022`, since otherwise os.target() reports "windows" and Coral's nethost probe hunts for win-* runtime packs and aborts. - Add Linux-GenProjects.sh for the sample game's standalone script workspace, which the root generation does not cover. - Stop tracking LuxSample.csproj and ScriptCore.csproj now that both can be generated on Linux; the blanket **.csproj ignore applies again and regeneration no longer dirties the tree. - Force Copy Local off for assembly references in the sample project via Directory.Build.props. premake cannot emit , and copying ScriptCore next to the game assembly makes the load context resolve a second ScriptCore, giving scripts a different Lux.Entity identity so IsSubclassOf fails and they silently never run. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 7 +- .gitignore | 5 - .../Assets/Scripts/Directory.Build.props | 12 ++ .../Assets/Scripts/Linux-GenProjects.sh | 25 ++++ .../Assets/Scripts/LuxSample.csproj | 45 ------ ScriptCore/ScriptCore.csproj | 18 --- ScriptCore/premake5.lua | 9 +- scripts/Linux-Build.sh | 130 ++++++++++++++---- 8 files changed, 153 insertions(+), 98 deletions(-) create mode 100755 Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh delete mode 100644 Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj delete mode 100644 ScriptCore/ScriptCore.csproj mode change 100644 => 100755 scripts/Linux-Build.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9bf79e0e..2de828a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -75,10 +75,15 @@ jobs: working-directory: ${{env.GITHUB_WORKSPACE}} run: New-Item -ItemType Directory -Force logs + # /restore runs the Restore target before the build. ScriptCore and Coral.Managed are + # SDK-style C# projects inside Lux.sln, and those cannot build without the + # obj/project.assets.json that a NuGet restore produces (error NETSDK1004). The same + # properties are passed as for the build, so the solution's per-project platform mapping + # resolves identically for both. - name: Build working-directory: ${{env.GITHUB_WORKSPACE}} run: | - MSBuild "${{ env.SOLUTION_FILE_PATH }}" /m "/p:Configuration=${{ matrix.configuration }}" "/p:Platform=${{ env.BUILD_PLATFORM }}" /fl "/flp:logfile=logs\Build-${{ matrix.configuration }}.log;verbosity=normal" + MSBuild "${{ env.SOLUTION_FILE_PATH }}" /restore /m "/p:Configuration=${{ matrix.configuration }}" "/p:Platform=${{ env.BUILD_PLATFORM }}" /fl "/flp:logfile=logs\Build-${{ matrix.configuration }}.log;verbosity=normal" - name: Package editor artifact if: matrix.configuration != 'Dist' diff --git a/.gitignore b/.gitignore index cdf606a6..fd167ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -39,11 +39,6 @@ premake5 **.csproj **.csproj.user -# ...except the hand-authored SDK-style projects. These are sources, not premake output: -# the Linux build drives them with `dotnet build` (premake's C# generator needs csc). -!ScriptCore/ScriptCore.csproj -!Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj - # Directories scripts/__pycache__ diff --git a/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props b/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props index 24fcb613..8a9b65cd 100644 --- a/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props +++ b/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props @@ -6,4 +6,16 @@ Intermediates/ + + + + + false + + diff --git a/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh b/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh new file mode 100755 index 00000000..4c3012d7 --- /dev/null +++ b/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Linux counterpart to Win-GenProjects.bat. +# +# The "vs2022" action is not Windows-only here: premake emits an SDK-style .csproj +# () that `dotnet build` consumes on any platform. +# The gmake2 action is deliberately NOT used for C# - it shells out to csc, which the +# .NET SDK does not put on PATH. +# +# The editor's ScriptBuilder runs `dotnet build` on the generated LuxSample.csproj. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PREMAKE="$(cd "$SCRIPT_DIR/../../../.." && pwd)/premake5" + +if [ ! -x "$PREMAKE" ]; then + echo "premake5 not found or not executable at: $PREMAKE" + exit 1 +fi + +cd "$SCRIPT_DIR" +"$PREMAKE" vs2022 + +echo "Generated LuxSample.csproj. Build it with:" +echo " dotnet build -c Release \"$SCRIPT_DIR/LuxSample.csproj\"" diff --git a/Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj b/Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj deleted file mode 100644 index 631a52e0..00000000 --- a/Editor/LuxSampleProject/Assets/Scripts/LuxSample.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - Library - net9.0 - LuxSample - LuxSample - true - disable - false - true - Binaries - - - - - - false - - - - - - - - - - - ../../../../Editor/Resources/Scripts/ScriptCore.dll - false - - - ../../../../Editor/DotNet/Coral.Managed.dll - false - - - diff --git a/ScriptCore/ScriptCore.csproj b/ScriptCore/ScriptCore.csproj deleted file mode 100644 index fcd2220a..00000000 --- a/ScriptCore/ScriptCore.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - Library - net9.0 - true - enable - false - true - ../Editor/Resources/Scripts - ../Editor/Resources/Scripts/Intermediates/ - - - - - ../Editor/DotNet/Coral.Managed.dll - - - diff --git a/ScriptCore/premake5.lua b/ScriptCore/premake5.lua index b16a4718..3ab79053 100644 --- a/ScriptCore/premake5.lua +++ b/ScriptCore/premake5.lua @@ -1,8 +1,13 @@ -- ScriptCore is a premake-generated C# project built inside Lux.sln, mirroring Hazel-ScriptCore. -- It links Coral.Managed (the C# host assembly) and lands in Editor/Resources/Scripts, where -- ScriptEngine loads the core assembly from. --- On Linux, ScriptCore is built via `dotnet build` in scripts/Linux-Build.sh instead of gmake. -if os.host() == "linux" then return end +-- On Linux, skip this project for the gmake actions only: premake's C# gmake generator shells +-- out to `csc`, which the .NET SDK does not put on PATH. The vs2022 action is still wanted +-- there - it emits an SDK-style .csproj that `dotnet build` consumes on any platform, which is +-- how scripts/Linux-Build.sh builds ScriptCore. +if os.host() == "linux" and _ACTION ~= nil and string.match(_ACTION, "^gmake") then + return +end project "ScriptCore" kind "SharedLib" diff --git a/scripts/Linux-Build.sh b/scripts/Linux-Build.sh old mode 100644 new mode 100755 index 52c42977..6f62309c --- a/scripts/Linux-Build.sh +++ b/scripts/Linux-Build.sh @@ -1,13 +1,21 @@ #!/bin/sh +# +# One-shot Linux build: submodules, C# projects, native engine, and the sample game's scripts. +# Safe to re-run; every step is idempotent. +# +# ./scripts/Linux-Build.sh # prompts for a configuration +# ./scripts/Linux-Build.sh release # non-interactive +# JOBS=1 ./scripts/Linux-Build.sh dist # serial build +# +# Extra arguments are forwarded to the first make invocation. set -e -if [ -n "${LUX_DIR+set}" ] - then - true - else - export LUX_DIR=$(realpath .) -fi +# Resolve the repo root from this script's own location so the script works from any directory. +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +LUX_DIR=${LUX_DIR:-$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)} +export LUX_DIR +cd "$LUX_DIR" if [ -n "${BUILD_CONFIG+set}" ] then @@ -43,30 +51,98 @@ if [ -n "${BUILD_CONFIG+set}" ] esac fi +CONFIG=$(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') +JOBS=${JOBS:-$(nproc 2>/dev/null || echo 1)} + +step() { echo; echo "==> $1"; } + +# --------------------------------------------------------------------------- +step "Checking prerequisites" + +missing="" +for tool in dotnet make clang pkg-config; do + command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool" +done +if [ -n "$missing" ]; then + echo "Missing required tool(s):$missing" + echo "On Arch: sudo pacman -S --needed dotnet-sdk make clang pkgconf" + exit 1 +fi + +if ! pkg-config --exists gtk+-3.0; then + echo "gtk+-3.0 development files not found (needed by NFD-Extended for file dialogs)." + echo "On Arch: sudo pacman -S --needed gtk3" + exit 1 +fi + +# --------------------------------------------------------------------------- +step "Syncing submodules" + +if [ -d .git ] && command -v git >/dev/null 2>&1; then + git submodule update --init --recursive +else + echo "Not a git checkout - skipping." +fi + if [ -n "${VULKAN_SDK+set}" ] then true else - export VULKAN_SDK=$(realpath Core/vendor/VulkanSDK/x86_64) + export VULKAN_SDK="$LUX_DIR/Core/vendor/VulkanSDK/x86_64" +fi + +if [ ! -d "$VULKAN_SDK" ]; then + echo "Vulkan SDK not found at: $VULKAN_SDK" + echo "Set VULKAN_SDK to a valid SDK, or check out the vendored one." + exit 1 +fi + +# --------------------------------------------------------------------------- +step "Generating C# projects" + +# premake's gmake2 C# generator shells out to `csc`, which the .NET SDK does not put on PATH, +# so use the vs2022 action instead - it emits SDK-style .csproj files that `dotnet build` +# consumes on any platform. --os=linux is required: without it os.target() reports "windows" +# and Coral's nethost probe looks for win-* runtime packs and aborts. +./premake5 --os=linux vs2022 + +# --------------------------------------------------------------------------- +step "Building managed assemblies ($BUILD_CONFIG)" + +# Coral.Managed is the C# host assembly; ScriptCore is the scripting API that games reference. +dotnet build -c Release --property WarningLevel=0 \ + Core/vendor/Coral/Coral.Managed/Coral.Managed-Static.csproj -o Editor/DotNet +dotnet build -c "$BUILD_CONFIG" --property WarningLevel=0 ScriptCore/ScriptCore.csproj + +# Core's postbuild step expects Coral's artifacts here. +mkdir -p Core/vendor/Coral/Build/Release +cp -f Editor/DotNet/Coral.Managed.dll Core/vendor/Coral/Build/Release/ +cp -f Editor/DotNet/Coral.Managed.runtimeconfig.json Core/vendor/Coral/Build/Release/ 2>/dev/null || true +cp -f Editor/DotNet/Coral.Managed.deps.json Core/vendor/Coral/Build/Release/ 2>/dev/null || true + +# --------------------------------------------------------------------------- +step "Building engine ($BUILD_CONFIG, -j$JOBS)" + +# ScriptCore is skipped by the gmake action on Linux (built above via dotnet). +./premake5 gmake2 --cc=clang +make -j"$JOBS" config="$CONFIG" Dependencies Dependencies/Renderer "$@" +make -j"$JOBS" -C Core -f Makefile config="$CONFIG" +make -j"$JOBS" -C Editor -f Makefile config="$CONFIG" +make -j"$JOBS" -C Lux-Runtime -f Makefile config="$CONFIG" + +# --------------------------------------------------------------------------- +# The sample game's scripts are a standalone premake workspace, so the root generation above +# does not cover them. Without this the editor reports "Script project file not found" on a +# fresh clone, because the .csproj is generated output and is not checked in. +SAMPLE_SCRIPTS="$LUX_DIR/Editor/LuxSampleProject/Assets/Scripts" +if [ -f "$SAMPLE_SCRIPTS/premake5.lua" ]; then + step "Building sample project scripts ($BUILD_CONFIG)" + "$SAMPLE_SCRIPTS/Linux-GenProjects.sh" >/dev/null + dotnet build -c "$BUILD_CONFIG" --property WarningLevel=0 "$SAMPLE_SCRIPTS/LuxSample.csproj" fi -# Build Coral.Managed (the C# host assembly) into Editor/DotNet, and the ScriptCore -# assembly into Editor/Resources/Scripts. Both are SDK-style net9.0 projects built with -# the dotnet CLI. ScriptCore references Coral.Managed, so building it also builds Coral.Managed. - dotnet build -c Release --property WarningLevel=0 \ - Core/vendor/Coral/Coral.Managed/Coral.Managed-Static.csproj -o Editor/DotNet - dotnet build -c $BUILD_CONFIG --property WarningLevel=0 ScriptCore/ScriptCore.csproj - -# Ensure Coral build artifacts exist where the Core postbuild step expects them - mkdir -p Core/vendor/Coral/Build/Release - cp -f Editor/DotNet/Coral.Managed.dll Core/vendor/Coral/Build/Release/ - cp -f Editor/DotNet/Coral.Managed.runtimeconfig.json Core/vendor/Coral/Build/Release/ 2>/dev/null || true - cp -f Editor/DotNet/Coral.Managed.deps.json Core/vendor/Coral/Build/Release/ 2>/dev/null || true - -# Build Lux (skip the gmake ScriptCore target — it was already built above via dotnet) - ./premake5 gmake2 --cc=clang - CONFIG=$(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') - make config=$CONFIG Dependencies Dependencies/Renderer "$@" - make -C Core -f Makefile config=$CONFIG - make -C Editor -f Makefile config=$CONFIG - make -C Lux-Runtime -f Makefile config=$CONFIG +# --------------------------------------------------------------------------- +echo +echo "==> Done ($BUILD_CONFIG)" +echo " Editor: ./scripts/Linux-Run.sh $CONFIG" +echo " Runtime: ./scripts/Linux-RunRuntime.sh $CONFIG" From 663b2c68d03fb4ce4b9ba3cd17c89b57e6340e64 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 18:40:27 -0400 Subject: [PATCH 13/20] CI: treat editor state files as optional when packaging the artifact The Debug and Release jobs failed with "Editor artifact input was not found: Editor\App.lsettings". That file was untracked in e8a9464 along with the other editor state files, so it does not exist in a clean checkout - the packaging step still listed it as a required input. Dist passed only because the step is skipped for that configuration, and the failure was masked until now because the build itself failed earlier on the missing NuGet restore. imgui.ini and App.lsettings are regenerated on first run (ApplicationSettings falls back to defaults when the file is absent), so they are copied when present and no longer fail the build. LuxSampleProject, Resources, and DotNet are real build inputs and stay strict. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2de828a1..403a7952 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,13 +93,19 @@ jobs: $editorOutput = Join-Path "bin" "$configuration-windows-x86_64\Editor" $artifactRoot = Join-Path "artifacts" "Editor-$configuration" $editorItems = @( - "imgui.ini", - "App.lsettings", "LuxSampleProject", "Resources", "DotNet" ) + # Editor state, not build input: both are regenerated on first run (ApplicationSettings + # falls back to defaults when App.lsettings is absent) and are gitignored, so they do + # not exist in a clean checkout. Copy them when present, never fail on them. + $optionalEditorItems = @( + "imgui.ini", + "App.lsettings" + ) + if (!(Test-Path $editorOutput)) { throw "Editor build output was not found: $editorOutput" } @@ -118,6 +124,15 @@ jobs: Copy-Item -Path $source -Destination $artifactRoot -Recurse -Force } + foreach ($item in $optionalEditorItems) { + $source = Join-Path "Editor" $item + if (Test-Path $source) { + Copy-Item -Path $source -Destination $artifactRoot -Recurse -Force + } else { + Write-Host "Skipping optional editor state file (not present): $source" + } + } + - name: Upload editor artifact if: matrix.configuration != 'Dist' uses: actions/upload-artifact@v4 From 747c629c07fabd9b7eef6afbdf8489ca5993da6c Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 19:07:13 -0400 Subject: [PATCH 14/20] CI: add a Linux build job CI was Windows-only, so nothing stopped the Linux port from rotting the first time shared engine code changed. Mirrors the Windows job: ubuntu-24.04 across the same Debug/Release/Dist matrix, with fail-fast off. The build step is a single call to scripts/Linux-Build.sh, which generates the C# projects, builds the managed assemblies, the native engine, and the sample game's scripts. Dependencies were derived from the linked binary and the premake files rather than guessed: GLFW compiles both the X11 and Wayland backends and runs wayland-scanner during the build (libwayland-bin), NFD-Extended needs GTK3 for file dialogs, and backward-cpp resolves symbols through libdw/libunwind. The Vulkan SDK uses the same action and version as Windows - Core/vendor/VulkanSDK/ is gitignored so it is absent from a clean checkout, but Dependencies.lua and Linux-Build.sh both prefer the VULKAN_SDK the action exports. A verify step asserts the Editor and Lux-Runtime binaries exist afterwards, so a build that silently produces nothing cannot report success. All three configurations were built locally on Linux before enabling them here. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 403a7952..99e4bc59 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -148,3 +148,66 @@ jobs: name: build-logs-${{ matrix.configuration }} path: logs/*.log if-no-files-found: ignore + + build-linux: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release, Dist] + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + submodules: recursive + lfs: true + + # Coral hardcodes hostfxr major version 9, and ScriptCore targets net9.0. + - name: Set up .NET 9 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + # GLFW is built with both the X11 and Wayland backends and runs wayland-scanner during + # the build (wayland-scanner lives in libwayland-bin). NFD-Extended needs GTK3 for file + # dialogs, and backward-cpp resolves symbols through libdw/libunwind. + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + clang make pkg-config \ + libgtk-3-dev \ + libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev \ + libxkbcommon-dev libwayland-dev libwayland-bin wayland-protocols \ + libdw-dev libunwind-dev + + # Sets VULKAN_SDK, which Dependencies.lua and Linux-Build.sh both honour in preference + # to the (gitignored) vendored SDK path. + - name: Install Vulkan SDK + uses: jakoch/install-vulkan-sdk-action@v1.4.0 + with: + vulkan_version: 1.4.335.0 + optional_components: com.lunarg.vulkan.vma, com.lunarg.vulkan.debug, com.lunarg.vulkan.glm + install_runtime: true + cache: true + + # Single entry point: generates the C# projects, builds the managed assemblies, the + # native engine, and the sample game's scripts. + - name: Build + run: ./scripts/Linux-Build.sh ${{ matrix.configuration }} + + # The build script is `set -e`, but assert the artifacts exist so a step that silently + # produces nothing cannot pass. + - name: Verify build output + run: | + out="bin/${{ matrix.configuration }}-linux-x86_64" + fail=0 + for f in "$out/Editor/Editor" "$out/Lux-Runtime/Lux-Runtime"; do + if [ -x "$f" ]; then + echo "ok $f" + else + echo "MISSING $f"; fail=1 + fi + done + exit $fail From 416ed1fad646327880248e6cb88752cb5a8f1930 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 19:14:44 -0400 Subject: [PATCH 15/20] Bootstrap premake5 on Linux instead of assuming a local binary The Linux CI job failed with "./premake5: not found". Only vendor/bin/premake5.exe is committed, and .gitignore matches any file named "premake5", so a clean checkout has no Linux premake at all - the root binary only ever existed on developer machines. This affected every fresh clone, not just CI. Linux-Build.sh now resolves premake in order: a local ./premake5, then vendor/bin/premake5, otherwise it downloads the pinned 5.0.0-beta4 Linux release and verifies its SHA-256 before use. The pinned archive is byte-identical to the binary this port was developed against, so generation behaviour is unchanged. The downloaded binary lands in vendor/bin/premake5, which the existing ignore rule already covers. Linux-GenProjects.sh gets the same resolution order, and points at Linux-Build.sh when no binary is available. Also corrects CLAUDE.md, which claimed the binary was checked in at ./premake5. Verified by removing both binaries and running the script from a clean state: it downloads, verifies, and completes a full Release build. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 5 +- .../Assets/Scripts/Linux-GenProjects.sh | 13 +++-- scripts/Linux-Build.sh | 52 +++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 312cffa9..fe3a6553 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,10 @@ LuxEngine is a C++20, Vulkan-only 3D game engine and editor. It is a solo projec ## Build System -Premake5 is used to generate build files. The binary is checked in at `./premake5`. +Premake5 is used to generate build files. Only the Windows binary is committed +(`vendor/bin/premake5.exe`); any file named `premake5` is gitignored, so a clean checkout has no +Linux binary. `scripts/Linux-Build.sh` downloads a pinned, checksum-verified build into +`vendor/bin/premake5` on first run — prefer that script over invoking premake directly. **Generate Makefiles (Linux):** ```bash diff --git a/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh b/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh index 4c3012d7..44c7dbf4 100755 --- a/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh +++ b/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh @@ -11,10 +11,17 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PREMAKE="$(cd "$SCRIPT_DIR/../../../.." && pwd)/premake5" +ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -if [ ! -x "$PREMAKE" ]; then - echo "premake5 not found or not executable at: $PREMAKE" +# Only vendor/bin/premake5.exe is committed, so the Linux binary is either a local build at the +# repo root or the pinned one that scripts/Linux-Build.sh downloads into vendor/bin. +if [ -x "$ROOT/premake5" ]; then + PREMAKE="$ROOT/premake5" +elif [ -x "$ROOT/vendor/bin/premake5" ]; then + PREMAKE="$ROOT/vendor/bin/premake5" +else + echo "No Linux premake5 found (looked in $ROOT and $ROOT/vendor/bin)." + echo "Run ./scripts/Linux-Build.sh once - it downloads a pinned build." exit 1 fi diff --git a/scripts/Linux-Build.sh b/scripts/Linux-Build.sh index 6f62309c..a214efe8 100755 --- a/scripts/Linux-Build.sh +++ b/scripts/Linux-Build.sh @@ -60,12 +60,12 @@ step() { echo; echo "==> $1"; } step "Checking prerequisites" missing="" -for tool in dotnet make clang pkg-config; do +for tool in dotnet make clang pkg-config curl tar; do command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool" done if [ -n "$missing" ]; then echo "Missing required tool(s):$missing" - echo "On Arch: sudo pacman -S --needed dotnet-sdk make clang pkgconf" + echo "On Arch: sudo pacman -S --needed dotnet-sdk make clang pkgconf curl tar" exit 1 fi @@ -97,6 +97,50 @@ if [ ! -d "$VULKAN_SDK" ]; then exit 1 fi +# --------------------------------------------------------------------------- +step "Locating premake5" + +# Only the Windows binary (vendor/bin/premake5.exe) is committed, and the repo gitignores any +# file named "premake5" - so a clean checkout has no Linux premake at all. Fetch a pinned build +# on first run and verify it, rather than depending on whatever happens to be on the machine. +PREMAKE_VERSION=5.0.0-beta4 +PREMAKE_SHA256=4356ab7cdec6085183d68fb240089376eacdc2fb751ffbd8063d797ae43abeb3 + +if [ -x "$LUX_DIR/premake5" ]; then + PREMAKE="$LUX_DIR/premake5" + echo "Using $PREMAKE" +elif [ -x "$LUX_DIR/vendor/bin/premake5" ]; then + PREMAKE="$LUX_DIR/vendor/bin/premake5" + echo "Using $PREMAKE" +else + echo "No Linux premake5 found - downloading $PREMAKE_VERSION" + pm_tmp=$(mktemp -d) + pm_url="https://github.com/premake/premake-core/releases/download/v$PREMAKE_VERSION/premake-$PREMAKE_VERSION-linux.tar.gz" + + if ! curl -sSL --retry 3 --max-time 180 -o "$pm_tmp/premake.tar.gz" "$pm_url"; then + echo "Failed to download premake5 from: $pm_url" + rm -rf "$pm_tmp" + exit 1 + fi + + tar xzf "$pm_tmp/premake.tar.gz" -C "$pm_tmp" premake5 + pm_sha=$(sha256sum "$pm_tmp/premake5" | awk '{print $1}') + if [ "$pm_sha" != "$PREMAKE_SHA256" ]; then + echo "premake5 checksum mismatch - refusing to use it." + echo " expected $PREMAKE_SHA256" + echo " actual $pm_sha" + rm -rf "$pm_tmp" + exit 1 + fi + + mkdir -p "$LUX_DIR/vendor/bin" + mv "$pm_tmp/premake5" "$LUX_DIR/vendor/bin/premake5" + chmod +x "$LUX_DIR/vendor/bin/premake5" + rm -rf "$pm_tmp" + PREMAKE="$LUX_DIR/vendor/bin/premake5" + echo "Installed $PREMAKE" +fi + # --------------------------------------------------------------------------- step "Generating C# projects" @@ -104,7 +148,7 @@ step "Generating C# projects" # so use the vs2022 action instead - it emits SDK-style .csproj files that `dotnet build` # consumes on any platform. --os=linux is required: without it os.target() reports "windows" # and Coral's nethost probe looks for win-* runtime packs and aborts. -./premake5 --os=linux vs2022 +"$PREMAKE" --os=linux vs2022 # --------------------------------------------------------------------------- step "Building managed assemblies ($BUILD_CONFIG)" @@ -124,7 +168,7 @@ cp -f Editor/DotNet/Coral.Managed.deps.json Core/vendor/Coral/Build/Release/ 2>/ step "Building engine ($BUILD_CONFIG, -j$JOBS)" # ScriptCore is skipped by the gmake action on Linux (built above via dotnet). -./premake5 gmake2 --cc=clang +"$PREMAKE" gmake2 --cc=clang make -j"$JOBS" config="$CONFIG" Dependencies Dependencies/Renderer "$@" make -j"$JOBS" -C Core -f Makefile config="$CONFIG" make -j"$JOBS" -C Editor -f Makefile config="$CONFIG" From 9e1739c6b008ecf688d0077cf2dd87e1d8d4f0e8 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 19:32:21 -0400 Subject: [PATCH 16/20] Fix Log::PrintMessage against libstdc++ versions without std::format_string The Linux CI build failed compiling every LUX_CORE_* call: "no known conversion from std::format_string<...> to format_string_t (aka basic_string_view)". spdlog only aliases format_string_t to std::format_string when the standard library advertises __cpp_lib_format >= 202207L, and otherwise falls back to std::string_view (Core/vendor/spdlog/include/spdlog/common.h:151). PrintMessage hardcoded std::format_string and forwarded it straight to the logger, so it only compiled where that alias happened to match. It built on Arch (clang 22, new libstdc++) and on MSVC, which is why this went unnoticed until an Ubuntu 24.04 runner built it. Take spdlog::format_string_t instead, so the parameter follows whatever spdlog resolved to. PrintMessageTag and PrintAssertMessage deliberately keep std::format_string: they call std::format themselves and hand spdlog an already-formatted string. Verified both branches: the fix compiles against a simulated string_view alias (with the previous signature failing the same test, confirming it reproduces the CI error), and a local Core debug build still succeeds where the alias is std::format_string. Co-Authored-By: Claude Opus 5 --- Core/Source/Lux/Core/Log.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Core/Source/Lux/Core/Log.h b/Core/Source/Lux/Core/Log.h index 1e3785b0..7c2168f8 100644 --- a/Core/Source/Lux/Core/Log.h +++ b/Core/Source/Lux/Core/Log.h @@ -51,7 +51,13 @@ namespace Lux { static void SetDefaultTagSettings(); template - static void PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args); + // spdlog::format_string_t rather than std::format_string: this format string is forwarded + // straight to spdlog, and spdlog only aliases it to std::format_string when the standard + // library advertises __cpp_lib_format >= 202207L. On older libstdc++ (e.g. Ubuntu 24.04) + // it falls back to std::string_view, which a std::format_string will not convert to. + // PrintMessageTag/PrintAssertMessage below stay on std::format_string - they call + // std::format themselves and hand spdlog an already-formatted string. + static void PrintMessage(Log::Type type, Log::Level level, spdlog::format_string_t format, Args&&... args); template static void PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, std::format_string format, Args&&... args); @@ -142,7 +148,7 @@ namespace Lux { namespace Lux { template - void Log::PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args) + void Log::PrintMessage(Log::Type type, Log::Level level, spdlog::format_string_t format, Args&&... args) { auto it = s_EnabledTags.find(""); if (it == s_EnabledTags.end()) From 25a14ac5977a09c378ada93ca14a5f2d4f54e29b Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 19:48:44 -0400 Subject: [PATCH 17/20] CI: install tbb, zlib and llvm-ar for the Linux build Two link-stage failures on the Linux runner, both missing packages: cannot find -ltbb (Debug, Release) llvm-ar: not found (Dist) TBB is an explicit Linux-only dependency (Dependencies.lua:64). The llvm-ar case is config-specific: only Dist compiles with -flto, and premake's clang toolset then archives with llvm-ar because GNU ar cannot index LLVM bitcode - the other configurations use plain ar and got far enough to hit the tbb error instead. Adds libtbb-dev, zlib1g-dev (for -lz) and llvm, plus a fallback symlink because Ubuntu ships llvm-ar version-suffixed and premake invokes it unversioned. The package list was re-derived by extracting every -l flag from the generated makefiles rather than reacting to one error at a time; everything else is either already installed, vendored, or provided by the Vulkan SDK. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 99e4bc59..7ac19e96 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -176,11 +176,19 @@ jobs: run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ - clang make pkg-config \ + clang llvm make pkg-config \ libgtk-3-dev \ libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev \ libxkbcommon-dev libwayland-dev libwayland-bin wayland-protocols \ - libdw-dev libunwind-dev + libdw-dev libunwind-dev libtbb-dev zlib1g-dev + + # The Dist configuration compiles with -flto, and premake's clang toolset then archives + # with llvm-ar because GNU ar cannot index LLVM bitcode. Ubuntu ships that binary + # version-suffixed, so make sure an unversioned llvm-ar resolves on PATH. + if ! command -v llvm-ar >/dev/null 2>&1; then + sudo ln -sf "$(ls -1 /usr/bin/llvm-ar-* | sort -V | tail -1)" /usr/bin/llvm-ar + fi + llvm-ar --version | head -1 # Sets VULKAN_SDK, which Dependencies.lua and Linux-Build.sh both honour in preference # to the (gitignored) vendored SDK path. From 6f4df714934e8161bd1643adc5b933d788ffaefc Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 20:10:42 -0400 Subject: [PATCH 18/20] CI: upload artifacts for Release only, and add a Linux editor artifact Debug and Dist no longer upload an editor bundle or build logs on either platform; the upload steps are kept, just gated to Release. Windows previously uploaded for both Debug and Release. Linux had no artifacts at all, so it now matches Windows. Two differences the packaging has to account for: the Linux build output contains only the executable, and the binary is linked with RPATH $ORIGIN/lib - so Resources, DotNet and LuxSampleProject are copied alongside it and the vendored shared libraries are bundled into lib/. Verified locally: the resulting tree resolves every dependency with LD_LIBRARY_PATH unset, the vendored libs coming from the bundled lib/. System libraries (GTK3, TBB, ...) are still expected on the target machine, as with any distro build. The Linux build step now tees to logs/, with pipefail so a build failure is not masked by tee's exit status. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 68 +++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ac19e96..1f4f512e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,7 +86,7 @@ jobs: MSBuild "${{ env.SOLUTION_FILE_PATH }}" /restore /m "/p:Configuration=${{ matrix.configuration }}" "/p:Platform=${{ env.BUILD_PLATFORM }}" /fl "/flp:logfile=logs\Build-${{ matrix.configuration }}.log;verbosity=normal" - name: Package editor artifact - if: matrix.configuration != 'Dist' + if: matrix.configuration == 'Release' working-directory: ${{env.GITHUB_WORKSPACE}} run: | $configuration = "${{ matrix.configuration }}" @@ -134,7 +134,7 @@ jobs: } - name: Upload editor artifact - if: matrix.configuration != 'Dist' + if: matrix.configuration == 'Release' uses: actions/upload-artifact@v4 with: name: editor-${{ matrix.configuration }} @@ -142,7 +142,7 @@ jobs: if-no-files-found: error - name: Upload build logs - if: always() + if: always() && matrix.configuration == 'Release' uses: actions/upload-artifact@v4 with: name: build-logs-${{ matrix.configuration }} @@ -203,7 +203,13 @@ jobs: # Single entry point: generates the C# projects, builds the managed assemblies, the # native engine, and the sample game's scripts. - name: Build - run: ./scripts/Linux-Build.sh ${{ matrix.configuration }} + shell: bash + run: | + # pipefail so a build failure is not masked by tee's exit status. + set -o pipefail + mkdir -p logs + ./scripts/Linux-Build.sh ${{ matrix.configuration }} 2>&1 \ + | tee "logs/Build-${{ matrix.configuration }}.log" # The build script is `set -e`, but assert the artifacts exist so a step that silently # produces nothing cannot pass. @@ -219,3 +225,57 @@ jobs: fi done exit $fail + + # Unlike Windows, the Linux build output holds only the executable, and the binary is + # linked with RPATH $ORIGIN/lib - so the vendored shared libraries have to be bundled + # into lib/ for the artifact to be runnable at all. + - name: Package editor artifact + if: matrix.configuration == 'Release' + shell: bash + run: | + set -euo pipefail + out="bin/${{ matrix.configuration }}-linux-x86_64/Editor" + root="artifacts/Editor-linux-${{ matrix.configuration }}" + + rm -rf "$root" + mkdir -p "$root/lib" + cp -a "$out/." "$root/" + + for item in LuxSampleProject Resources DotNet; do + if [ ! -e "Editor/$item" ]; then + echo "Editor artifact input was not found: Editor/$item" >&2 + exit 1 + fi + cp -a "Editor/$item" "$root/" + done + + # Editor state, regenerated on first run and gitignored, so absent in a clean checkout. + for item in imgui.ini App.lsettings; do + [ -e "Editor/$item" ] && cp -a "Editor/$item" "$root/" \ + || echo "Skipping optional editor state file (not present): Editor/$item" + done + + for dir in "Core/vendor/assimp/bin/linux" \ + "Core/vendor/NvidiaAftermath/lib/x64/linux" \ + "${VULKAN_SDK:-}/lib"; do + [ -d "$dir" ] || continue + find "$dir" -maxdepth 1 -name '*.so*' -exec cp -aP {} "$root/lib/" \; + done + + echo "Bundled $(find "$root/lib" -name '*.so*' | wc -l) shared libraries." + + - name: Upload editor artifact + if: matrix.configuration == 'Release' + uses: actions/upload-artifact@v4 + with: + name: editor-linux-${{ matrix.configuration }} + path: artifacts/Editor-linux-${{ matrix.configuration }}/ + if-no-files-found: error + + - name: Upload build logs + if: always() && matrix.configuration == 'Release' + uses: actions/upload-artifact@v4 + with: + name: build-logs-linux-${{ matrix.configuration }} + path: logs/*.log + if-no-files-found: ignore From 9f52128e67ed666865aaee4c337bb1e688ee9c09 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 20:31:20 -0400 Subject: [PATCH 19/20] CI: trim the Linux editor artifact The artifact was 417 MB, more than double the Windows one, because packaging globbed every *.so out of the Vulkan SDK lib/ - roughly 780 MB of validation layers and SPIRV tooling the editor never loads. Only three vendored libraries are actually resolved. Bundle exactly what ldd reports instead. ldd gives the full transitive closure, so filtering it to our vendored directories keeps precisely what is needed and adapts to the runner (locally shaderc comes from the SDK, elsewhere it may come from the system). That takes lib/ from 38 libraries to 3, 27 MB. Also enables stripdown on the Linux Vulkan SDK install, matching Windows, and prunes build intermediates and asset caches from the bundled sample project. Scripts/Binaries is kept - it holds the sample game's compiled script assembly. The step now asserts the bundle resolves with LD_LIBRARY_PATH unset, since the binary's RPATH is $ORIGIN/lib; otherwise we could ship an artifact that only runs on a machine that already has these libraries. Verified locally by running the packaging step as written: 3 libraries bundled, zero unresolved dependencies. Co-Authored-By: Claude Opus 5 --- .github/workflows/main.yml | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1f4f512e..e04c7e0c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -199,6 +199,7 @@ jobs: optional_components: com.lunarg.vulkan.vma, com.lunarg.vulkan.debug, com.lunarg.vulkan.glm install_runtime: true cache: true + stripdown: true # Single entry point: generates the C# projects, builds the managed assemblies, the # native engine, and the sample game's scripts. @@ -249,20 +250,51 @@ jobs: cp -a "Editor/$item" "$root/" done + # Drop build intermediates and asset caches that the build regenerates. Scripts/Binaries + # is deliberately kept - it holds the sample game's compiled script assembly. + find "$root/LuxSampleProject" -type d \ + \( -name Intermediates -o -name obj -o -name Cache \) -prune -exec rm -rf {} + 2>/dev/null || true + # Editor state, regenerated on first run and gitignored, so absent in a clean checkout. for item in imgui.ini App.lsettings; do [ -e "Editor/$item" ] && cp -a "Editor/$item" "$root/" \ || echo "Skipping optional editor state file (not present): Editor/$item" done + # Bundle only the libraries the binary actually resolves out of our vendored + # directories. Globbing every *.so from the Vulkan SDK pulled in ~780 MB of + # validation layers and SPIRV tooling that the editor never loads; ldd reports the + # full transitive closure, so filtering it keeps exactly what is needed. + vendor_dirs="" for dir in "Core/vendor/assimp/bin/linux" \ "Core/vendor/NvidiaAftermath/lib/x64/linux" \ "${VULKAN_SDK:-}/lib"; do - [ -d "$dir" ] || continue - find "$dir" -maxdepth 1 -name '*.so*' -exec cp -aP {} "$root/lib/" \; + if [ -d "$dir" ]; then + vendor_dirs="$vendor_dirs $(cd "$dir" && pwd)" + fi + done + + # So ldd can resolve the vendored libraries, which are not on the default search path. + export LD_LIBRARY_PATH="$(echo $vendor_dirs | tr ' ' ':')" + + for lib in $(ldd "$root/Editor" | awk '$3 ~ /^\// {print $3}' | sort -u); do + for dir in $vendor_dirs; do + case "$lib" in + "$dir"/*) cp -aL "$lib" "$root/lib/"; break ;; + esac + done done - echo "Bundled $(find "$root/lib" -name '*.so*' | wc -l) shared libraries." + echo "Bundled $(find "$root/lib" -type f | wc -l) shared libraries ($(du -sh "$root/lib" | cut -f1))." + + # The binary's RPATH is $ORIGIN/lib, so the bundle must resolve with no + # LD_LIBRARY_PATH set - otherwise we would ship an artifact that only runs on a + # machine that already has these libraries. + if env -u LD_LIBRARY_PATH ldd "$root/Editor" | grep -q 'not found'; then + echo "Editor artifact has unresolved libraries:" >&2 + env -u LD_LIBRARY_PATH ldd "$root/Editor" | grep 'not found' >&2 + exit 1 + fi - name: Upload editor artifact if: matrix.configuration == 'Release' From 5cf9a0542e6537af163ede2a4a8ef76c6100d735 Mon Sep 17 00:00:00 2001 From: Pier-Olivier Boulianne Date: Tue, 28 Jul 2026 20:57:47 -0400 Subject: [PATCH 20/20] Scale the editor UI for HiDPI / fractional-scaling displays The editor was unusable on a 150% 4K monitor, in two different ways depending on the windowing system. ImGui_ImplGlfw_NewFrame derives DisplaySize from glfwGetWindowSize and DisplayFramebufferScale from the framebuffer ratio, so: X11/Windows - the window size is already in physical pixels, framebuffer scale is 1, and nothing is scaled: a 15px font on a 4K panel is physically tiny. Wayland - the window size is logical and framebuffer scale is 1.5, so geometry is the right size but glyphs baked at 15px are stretched 1.5x and blur. ImGui is 1.92.6 and can re-bake fonts on demand, but that needs ImGuiBackendFlags_RendererHasTextures, which our NVRHI renderer does not advertise - it only sets RendererHasVtxOffset and RendererHasViewports. So the atlas is static and io.ConfigDpiScaleFonts would merely stretch it. Glyphs have to be baked at the physical size they will be drawn at instead: bake at size * contentScale glyphs rasterised at true physical pixels FontScaleMain = 1 / fbScale undo scaling the compositor already applies ScaleAllSizes(contentScale/fbScale) padding and rounding in DisplaySize units Both cases then resolve correctly: on X11 fonts and metrics scale together, on Wayland fonts bake larger while layout stays in logical units. LUX_UI_SCALE overrides the detected scale, and the values are logged at startup, so a wrong detection can be diagnosed and worked around without a rebuild. Verified: at scale 1.0 the path is a no-op, so nothing changes for existing setups; LUX_UI_SCALE=1.5 reports contentScale=1.50 fbScale=1.00 uiScale=1.50 and the editor runs clean. The visual result still needs confirming on an actual HiDPI display. Co-Authored-By: Claude Opus 5 --- Core/Source/Lux/ImGui/ImGuiFonts.cpp | 17 ++++++- Core/Source/Lux/ImGui/ImGuiFonts.h | 7 +++ Core/Source/Lux/ImGui/ImGuiLayer.cpp | 71 ++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Core/Source/Lux/ImGui/ImGuiFonts.cpp b/Core/Source/Lux/ImGui/ImGuiFonts.cpp index 99830cc8..7539337d 100644 --- a/Core/Source/Lux/ImGui/ImGuiFonts.cpp +++ b/Core/Source/Lux/ImGui/ImGuiFonts.cpp @@ -4,6 +4,18 @@ namespace Lux::ImGuiEx { static std::unordered_map s_Fonts; + static float s_Scale = 1.0f; + + void Fonts::SetScale(float scale) + { + if (scale > 0.0f) + s_Scale = scale; + } + + float Fonts::GetScale() + { + return s_Scale; + } void Fonts::Add(const FontConfiguration& config, bool isDefault) { @@ -16,7 +28,10 @@ namespace Lux::ImGuiEx { ImFontConfig imguiFontConfig; imguiFontConfig.MergeMode = config.MergeWithLast; auto& io = ImGui::GetIO(); - ImFont* font = io.Fonts->AddFontFromFileTTF(config.FilePath.data(), config.Size, &imguiFontConfig, config.GlyphRanges == nullptr ? io.Fonts->GetGlyphRangesDefault() : config.GlyphRanges); + // Bake at the physical pixel size the glyphs will be drawn at; ImGuiLayer divides the + // scale back out via style.FontScaleMain so layout stays in DisplaySize units. + const float bakedSize = config.Size * s_Scale; + ImFont* font = io.Fonts->AddFontFromFileTTF(config.FilePath.data(), bakedSize, &imguiFontConfig, config.GlyphRanges == nullptr ? io.Fonts->GetGlyphRangesDefault() : config.GlyphRanges); LUX_CORE_VERIFY(font, "Failed to load font file!"); s_Fonts[config.FontName] = font; diff --git a/Core/Source/Lux/ImGui/ImGuiFonts.h b/Core/Source/Lux/ImGui/ImGuiFonts.h index 8777b416..9465e312 100644 --- a/Core/Source/Lux/ImGui/ImGuiFonts.h +++ b/Core/Source/Lux/ImGui/ImGuiFonts.h @@ -16,6 +16,13 @@ namespace Lux::ImGuiEx { class Fonts { public: + // Multiplier applied to every FontConfiguration::Size at bake time, so glyphs are + // rasterised at the physical pixel size they will actually be drawn at on a HiDPI + // display. Must be set before any Add() call - the atlas is static, so sizes cannot + // change afterwards. See ImGuiLayer::OnAttach for how the value is derived. + static void SetScale(float scale); + static float GetScale(); + static void Add(const FontConfiguration& config, bool isDefault = false); static void PushFont(const std::string& fontName); static void PopFont(); diff --git a/Core/Source/Lux/ImGui/ImGuiLayer.cpp b/Core/Source/Lux/ImGui/ImGuiLayer.cpp index c499c387..8889c348 100644 --- a/Core/Source/Lux/ImGui/ImGuiLayer.cpp +++ b/Core/Source/Lux/ImGui/ImGuiLayer.cpp @@ -22,6 +22,10 @@ #include "backends/imgui_impl_glfw.h" +#include + +#include + // TODO(Yan): WIP // Defined in imgui_impl_glfw.cpp // extern bool g_DisableImGuiEvents; @@ -41,6 +45,62 @@ namespace Lux { io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows #endif + // --------------------------------------------------------------------------------- + // HiDPI / fractional scaling. + // + // Two quantities matter and they are not the same thing: + // contentScale - what the desktop asks for (1.5 on a "150%" monitor). + // fbScale - framebuffer pixels per DisplaySize unit, i.e. how much scaling the + // windowing system already applies for us. + // + // On X11/Windows GLFW reports the window size in physical pixels, so fbScale is 1 and + // nothing is scaled for us - the UI ends up tiny on a 4K panel. On Wayland the window + // size is logical and fbScale is 1.5, so geometry is already the right physical size but + // a font baked at 15px gets stretched 1.5x and looks blurry. + // + // Our ImGui renderer does not advertise ImGuiBackendFlags_RendererHasTextures, so the + // font atlas is static and ImGui cannot re-bake glyphs on the fly. That rules out + // io.ConfigDpiScaleFonts (it would just stretch the existing atlas). Instead bake the + // glyphs at the physical size they will be drawn at, then divide back out. + // + // bake at size * contentScale -> rasterised at true physical pixels (crisp) + // FontScaleMain = 1 / fbScale -> undo scaling the compositor already does + // ScaleAllSizes(uiScale) -> padding/rounding follow DisplaySize units + // + // LUX_UI_SCALE overrides the detected value, for testing or when detection is wrong. + float contentScale = 1.0f; + float fbScale = 1.0f; + { + auto* window = (GLFWwindow*)Application::Get().GetWindow().GetNativeWindow(); + if (window) + { + float sx = 1.0f, sy = 1.0f; + glfwGetWindowContentScale(window, &sx, &sy); + if (sx > 0.0f) + contentScale = sx; + + int winW = 0, winH = 0, fbW = 0, fbH = 0; + glfwGetWindowSize(window, &winW, &winH); + glfwGetFramebufferSize(window, &fbW, &fbH); + if (winW > 0 && fbW > 0) + fbScale = (float)fbW / (float)winW; + } + + if (const char* env = std::getenv("LUX_UI_SCALE")) + { + const float override = std::strtof(env, nullptr); + if (override > 0.0f) + contentScale = override; + } + } + + const float uiScale = (fbScale > 0.0f) ? (contentScale / fbScale) : contentScale; + LUX_CORE_INFO("[ImGui] contentScale={:.2f} framebufferScale={:.2f} -> uiScale={:.2f}", + contentScale, fbScale, uiScale); + + // Every font is baked at its declared size multiplied by this. + ImGuiEx::Fonts::SetScale(contentScale); + // Configure Fonts { ImGuiEx::FontConfiguration robotoBold; @@ -114,6 +174,17 @@ namespace Lux { style.Colors[ImGuiCol_WindowBg] = windowBg; } + // Apply the DPI scaling worked out above. Must come after the style is fully configured: + // ScaleAllSizes multiplies the current padding/rounding/spacing values in place, so any + // size assigned afterwards would escape the scaling. + // + // Fonts were baked at contentScale, while ImGui lays out in DisplaySize units - divide + // out whatever the windowing system already scales so text is the right size on both a + // physical-pixel (X11/Windows) and a logical-unit (Wayland) surface. + style.FontScaleMain = (fbScale > 0.0f) ? (1.0f / fbScale) : 1.0f; + if (uiScale != 1.0f) + style.ScaleAllSizes(uiScale); + ImGui_ImplGlfw_InitForVulkan((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow(), true); m_ImGuiRenderer = std::make_unique();