diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9bf79e0e..e04c7e0c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -75,26 +75,37 @@ 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' + if: matrix.configuration == 'Release' working-directory: ${{env.GITHUB_WORKSPACE}} run: | $configuration = "${{ matrix.configuration }}" $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" } @@ -113,8 +124,17 @@ 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' + if: matrix.configuration == 'Release' uses: actions/upload-artifact@v4 with: name: editor-${{ matrix.configuration }} @@ -122,9 +142,172 @@ 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 }} 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 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 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. + - 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 + stripdown: true + + # Single entry point: generates the C# projects, builds the managed assemblies, the + # native engine, and the sample game's scripts. + - name: Build + 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. + - 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 + + # 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 + + # 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 + 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" -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' + 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 diff --git a/.gitignore b/.gitignore index 52760f07..fd167ffb 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 @@ -28,6 +52,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 @@ -41,6 +68,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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..fe3a6553 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,142 @@ +# 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. 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 +./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. 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.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.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/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/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/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.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 6c689e16..7c2168f8 100644 --- a/Core/Source/Lux/Core/Log.h +++ b/Core/Source/Lux/Core/Log.h @@ -50,13 +50,14 @@ 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 + // 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); @@ -146,15 +147,13 @@ 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 + void Log::PrintMessage(Log::Type type, Log::Level level, spdlog::format_string_t 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(); @@ -183,7 +182,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(); @@ -212,7 +214,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/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 6fea87ac..eb749cb9 100644 --- a/Core/Source/Lux/Core/Memory.h +++ b/Core/Source/Lux/Core/Memory.h @@ -117,16 +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 -#warning "Memory tracking not available on non-Windows platform" -#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/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/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/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..32c8009d 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. @@ -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 @@ -311,8 +314,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); @@ -328,6 +345,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 +530,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 +575,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..10f8ebcc 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,8 +94,10 @@ namespace Lux { { std::string Title; uint32_t Width, Height; + bool SizeDirty = false; EventCallbackFn EventCallback; + Window* Self = nullptr; }; WindowData m_Data; 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/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/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 3bded64c..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; @@ -37,7 +41,65 @@ 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 + + // --------------------------------------------------------------------------------- + // 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 { @@ -112,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(); 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); } 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/ShaderCompiler/VulkanShaderCompiler.cpp b/Core/Source/Lux/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp index 7d6f105d..a4873ca8 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 @@ -353,7 +354,19 @@ 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. Hash source + global macros so the shader + // cache invalidates when either changes. m_StagesMetadata[stage] = StageData{}; + 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; @@ -444,18 +457,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. + // 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[] = "hazel-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), @@ -467,45 +484,83 @@ namespace Lux { "-I", "Resources/Shaders/Include/Common", "-I", "Resources/Shaders/Include/HLSL", - "-Fo", tempfileName + "-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"); 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); - // 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) { - return std::format("Could not execute `{}` for shader compilation: {} {}", exec[0], m_ShaderSourcePath.string(), ShaderUtils::ShaderStageToString(stage)); + 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(), ShaderUtils::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 @@ -745,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/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/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/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp index 6a7c72f5..7b314dab 100644 --- a/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp +++ b/Core/Source/Lux/Platform/Vulkan/VulkanSwapChain.cpp @@ -42,6 +42,41 @@ namespace Lux { VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::Get().GetGraphicsDeviceManager(); + vk::SurfaceCapabilitiesKHR surfaceCaps; + vk::Result capsRes = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfaceCapabilitiesKHR(m_Surface, &surfaceCaps); + if (capsRes != vk::Result::eSuccess) + { + LUX_CORE_ERROR("VulkanSwapChain::Create - getSurfaceCapabilitiesKHR failed: {}", nvrhi::vulkan::resultToString(VkResult(capsRes))); + 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."); + 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 @@ -49,6 +84,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) }; @@ -57,21 +100,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); @@ -101,12 +176,18 @@ 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 - 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; @@ -128,20 +209,26 @@ 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(); + return true; } void VulkanSwapChain::Destroy() @@ -220,13 +307,28 @@ 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); - m_Width = surfaceCaps.currentExtent.width; - m_Height = surfaceCaps.currentExtent.height; + 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; + } + + // 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; Resize(); BackBufferResized(); @@ -239,7 +341,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() @@ -267,15 +369,15 @@ 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(); #ifndef _WIN32 - if (deviceParams.vsyncEnabled) + if (vulkanDeviceManager->m_DeviceParams.vsyncEnabled) { - m_PresentQueue.waitIdle(); + vulkanDeviceManager->m_PresentQueue.waitIdle(); } #endif 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/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/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/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/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/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/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/Core/premake5.lua b/Core/premake5.lua index e6484a2c..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"', } @@ -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/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 diff --git a/Dependencies.lua b/Dependencies.lua index 2d89f6eb..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 @@ -55,22 +56,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 +151,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 +158,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", @@ -177,8 +168,8 @@ Dependencies = { IncludeDir = "%{wks.location}/Core/vendor/imgui", }, NVRHI = { - LibName = "NVRHI", - IncludeDir = "%{wks.location}/Core/vendor/NVRHI/include" + LibName = { "NVRHI", "NVRHI-Vulkan" }, + IncludeDir = "%{wks.location}/Core/vendor/nvrhi/include" }, MiniAudio = { IncludeDir = "%{wks.location}/Core/vendor/miniaudio/include", @@ -206,9 +197,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 +207,7 @@ Dependencies = { Windows = { LibName = "ws2_32", }, }, Dbghelp = { - Windows = { LibName = " Dbghelp" }, + Windows = { LibName = "Dbghelp" }, }, } 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/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..8a9b65cd --- /dev/null +++ b/Editor/LuxSampleProject/Assets/Scripts/Directory.Build.props @@ -0,0 +1,21 @@ + + + + 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..44c7dbf4 --- /dev/null +++ b/Editor/LuxSampleProject/Assets/Scripts/Linux-GenProjects.sh @@ -0,0 +1,32 @@ +#!/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)" +ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +# 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 + +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/LuxSample.luxproj b/Editor/LuxSampleProject/LuxSample.luxproj index c0e72617..1b4301d1 100644 --- a/Editor/LuxSampleProject/LuxSample.luxproj +++ b/Editor/LuxSampleProject/LuxSample.luxproj @@ -14,12 +14,12 @@ 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: 10706518493978196728 + IconHandle: 12258613209527478073 TargetConfig: Release SceneRenderer: Rendering: diff --git a/Editor/Source/EditorLayer.cpp b/Editor/Source/EditorLayer.cpp index 4d0884d7..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) @@ -1263,23 +922,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 +936,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,25 +978,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(); { - 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); + 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); @@ -2544,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"); @@ -2572,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); @@ -2602,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)) @@ -2618,6 +2300,7 @@ namespace Lux { if (extension == ".dll" || (extension == ".pdb" && targetConfig != RuntimeExportTarget::Dist)) CopyFileIfExists(entry.path(), exportRoot / entry.path().filename()); } +#endif } if (!resourcesSource.empty()) @@ -2653,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/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/Editor/Source/Panels/ProjectSettingsWindow.cpp b/Editor/Source/Panels/ProjectSettingsWindow.cpp index f35e96c8..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() @@ -362,10 +164,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/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/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 diff --git a/Editor/premake5.lua b/Editor/premake5.lua index 8c68ac28..7c580d0c 100644 --- a/Editor/premake5.lua +++ b/Editor/premake5.lua @@ -59,10 +59,19 @@ 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", "-Wl,-rpath,'$$ORIGIN/lib'" } - result, err = os.outputof("pkg-config --libs gtk+-3.0") - linkoptions { result } + -- 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 + local gtklibs, _ = os.outputof("pkg-config --libs gtk+-3.0") + linkoptions { gtklibs } + end filter "configurations:Debug or configurations:Debug-AS" symbols "On" 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..3ab79053 100644 --- a/ScriptCore/premake5.lua +++ b/ScriptCore/premake5.lua @@ -1,6 +1,14 @@ -- 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, 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" 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/premake5.lua b/premake5.lua index 04ff1db0..37e4cbd5 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", } @@ -90,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" @@ -102,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 @@ -134,7 +147,42 @@ group "Dependencies/Text" group "" group "Dependencies/Renderer" - include "Core/vendor/NVRHI" + -- 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-Build.sh b/scripts/Linux-Build.sh old mode 100644 new mode 100755 index 44760f36..a214efe8 --- a/scripts/Linux-Build.sh +++ b/scripts/Linux-Build.sh @@ -1,35 +1,192 @@ #!/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 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 + +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 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 curl tar" + 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 "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 -# 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 +# --------------------------------------------------------------------------- +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. +"$PREMAKE" --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). +"$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" +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 Lux - premake5 gmake --cc=clang --verbose - make config=$(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') "$@" +# --------------------------------------------------------------------------- +echo +echo "==> Done ($BUILD_CONFIG)" +echo " Editor: ./scripts/Linux-Run.sh $CONFIG" +echo " Runtime: ./scripts/Linux-RunRuntime.sh $CONFIG" diff --git a/scripts/Linux-Run.sh b/scripts/Linux-Run.sh old mode 100644 new mode 100755 index c60b4335..9462d3b8 --- a/scripts/Linux-Run.sh +++ b/scripts/Linux-Run.sh @@ -1,8 +1,42 @@ #!/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 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 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" "$@" 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.