From 54ba5271fce61bbdbb7761475b95e006ce54b32d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 18:23:40 +0000 Subject: [PATCH 1/9] fix: guard crash-prone paths surfaced by live engine/editor stress testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardens several runtime paths that crash or produce NaN state when fed unexpected input from users, corrupted assets, or zero-delta frames. Console commands — unguarded std::stof would throw and terminate the engine's command thread on non-numeric input: - weather_set intensity/transition - fov (camera FOV update) - vr_renderscale - autosave_interval Console commands — out-of-bounds array index when state/enum value exceeds name-table size: - weather_info (WeatherType) - accessibility_info (colorblindMode) - vr_info (trackingSpace, comfortMode) Runtime hot paths: - SkeletalAnimation::SampleClip: bounds-check boneIdx against outLocalTransforms.size() before indexing. A cached channel.boneIndex can outlive the producing skeleton (retargeting, runtime swap, bad clip data) and previously wrote past the end of the transform array. - ClothSimulation::IntegratePositions: skip integration on zero-delta ticks. The velocity formula (pos - prevPos) / dt would otherwise compute NaN on the first tick and poison downstream consumers. Also removes a dead Vulkan->OpenGL fallback block in SparkEngineLinux.cpp that ran before GraphicsEngine was constructed and so never executed. SDL2 submodule pointer updated to release-2.30.0 so the checkout matches what ThirdParty/SDL2/CMakeLists.txt expects (earlier state pointed at an unrelated SDL3 commit and broke configure). All 5863 SparkTests pass. Engine -headless and SparkEditor --test-mode both run clean under Xvfb/gVisor. --- SparkEngine/Source/Core/SparkEngineLinux.cpp | 32 ------------- .../Source/Core/SubsystemConsoleCommands.cpp | 17 +++++-- .../Core/SubsystemConsoleCommandsExt.cpp | 45 ++++++++++++++++--- .../Engine/Animation/SkeletalAnimation.cpp | 5 +++ .../Source/Physics/ClothSimulation.cpp | 4 ++ ThirdParty/SDL2 | 2 +- 6 files changed, 63 insertions(+), 42 deletions(-) diff --git a/SparkEngine/Source/Core/SparkEngineLinux.cpp b/SparkEngine/Source/Core/SparkEngineLinux.cpp index f69a4ddca..8e5182e2d 100644 --- a/SparkEngine/Source/Core/SparkEngineLinux.cpp +++ b/SparkEngine/Source/Core/SparkEngineLinux.cpp @@ -963,38 +963,6 @@ static int RunSDL2Windowed(int argc, char* argv[]) SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); } - // If the engine was created with a Vulkan window but GraphicsEngine - // fell back to OpenGL (Vulkan loaded but couldn't create a usable - // surface/device), recreate the window with OpenGL flags and create - // a GL context so the fallback backend can actually render. - if (preferVulkan && GetEngineRuntime().graphics) - { - auto* rhiDevice = GetEngineRuntime().graphics->GetRHIDevice(); - bool vulkanActive = rhiDevice && rhiDevice->GetBackendType() == Spark::RHI::GraphicsBackend::Vulkan; - auto* rhiBridge = GetEngineRuntime().graphics->GetRHIBridge(); - bool headless = rhiBridge && rhiBridge->IsHeadless(); - if (!vulkanActive && !headless) - { - Spark::SimpleConsole::GetInstance().LogInfo( - "Vulkan backend unavailable — recreating window with OpenGL support"); - SDL_DestroyWindow(window); - windowFlags = (windowFlags & ~SDL_WINDOW_VULKAN) | SDL_WINDOW_OPENGL; - window = SDL_CreateWindow("Spark Engine", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, winW, winH, - windowFlags); - if (window) - { - glContext = SDL_GL_CreateContext(window); - if (glContext) - { - SDL_GL_MakeCurrent(window, glContext); - SDL_GL_SetSwapInterval(1); - } - GetEngineRuntime().graphics->Shutdown(); - GetEngineRuntime().graphics->Initialize(static_cast(window)); - } - } - } - void* nativeRenderHandle = (preferMetal && sdlMetalView) ? sdlMetalView : nullptr; InitializeSDL2Subsystems(window, nativeRenderHandle, argc, argv); diff --git a/SparkEngine/Source/Core/SubsystemConsoleCommands.cpp b/SparkEngine/Source/Core/SubsystemConsoleCommands.cpp index a3451bd58..c2e8d36ed 100644 --- a/SparkEngine/Source/Core/SubsystemConsoleCommands.cpp +++ b/SparkEngine/Source/Core/SubsystemConsoleCommands.cpp @@ -408,8 +408,19 @@ namespace Spark if (it == types.end()) return "Unknown weather type. Options: clear, cloudy, rain, snow, fog, storm"; - float intensity = (args.size() > 1) ? std::stof(args[1]) : -1.0f; - float transition = (args.size() > 2) ? std::stof(args[2]) : 3.0f; + float intensity = -1.0f; + float transition = 3.0f; + try + { + if (args.size() > 1) + intensity = std::stof(args[1]); + if (args.size() > 2) + transition = std::stof(args[2]); + } + catch (const std::exception&) + { + return "Invalid numeric argument. Usage: weather_set [intensity] [transition_sec]"; + } weather->SetWeather(it->second, intensity, transition); return "Weather set to " + args[0]; }, @@ -427,7 +438,7 @@ namespace Spark int typeIdx = static_cast(state.type); std::stringstream ss; ss << "=== Weather State ===\n" - << " Type: " << (typeIdx < 6 ? typeNames[typeIdx] : "Unknown") << "\n" + << " Type: " << (typeIdx >= 0 && typeIdx < 6 ? typeNames[typeIdx] : "Unknown") << "\n" << " Intensity: " << state.intensity << "\n" << " Wind: speed=" << state.windSpeed << " dir=(" << state.windDirection.x << ", " << state.windDirection.y << ", " << state.windDirection.z << ")\n" diff --git a/SparkEngine/Source/Core/SubsystemConsoleCommandsExt.cpp b/SparkEngine/Source/Core/SubsystemConsoleCommandsExt.cpp index fd6fc0fd3..c309ee5b6 100644 --- a/SparkEngine/Source/Core/SubsystemConsoleCommandsExt.cpp +++ b/SparkEngine/Source/Core/SubsystemConsoleCommandsExt.cpp @@ -468,11 +468,20 @@ namespace Spark { if (args.empty()) return "FOV: " + EngineSettings::GetInstance().GetValue("Game", "FieldOfView"); + float fovValue = 0.0f; + try + { + fovValue = std::stof(args[0]); + } + catch (const std::exception&) + { + return "Invalid FOV value. Expected a number."; + } EngineSettings::GetInstance().SetValue("Game", "FieldOfView", args[0]); // Also update the live camera auto* cam = EngineContext::Get() ? EngineContext::Get()->GetCamera() : nullptr; if (cam) - cam->Console_SetFOV(std::stof(args[0])); + cam->Console_SetFOV(fovValue); return "FOV set to " + args[0]; }, "Get/set field of view", "Game"); @@ -532,9 +541,12 @@ namespace Spark { auto& a = EngineSettings::GetInstance().Accessibility(); const char* cbNames[] = {"Off", "Protanopia", "Deuteranopia", "Tritanopia"}; + const int cbCount = static_cast(sizeof(cbNames) / sizeof(cbNames[0])); std::stringstream ss; ss << "Accessibility Settings:\n"; - ss << " Colorblind Mode: " << cbNames[a.colorblindMode] << "\n"; + ss << " Colorblind Mode: " + << (a.colorblindMode >= 0 && a.colorblindMode < cbCount ? cbNames[a.colorblindMode] : "Unknown") + << "\n"; ss << " High Contrast: " << (a.highContrast ? "on" : "off") << "\n"; ss << " Large Text: " << (a.largeText ? "on" : "off") << "\n"; ss << " Reduce Motion: " << (a.reduceMotion ? "on" : "off") << "\n"; @@ -593,14 +605,21 @@ namespace Spark auto& vr = EngineSettings::GetInstance().VR(); const char* trackNames[] = {"Seated", "Room Scale"}; const char* comfortNames[] = {"Off", "Vignette", "Snap Turn"}; + const int trackCount = static_cast(sizeof(trackNames) / sizeof(trackNames[0])); + const int comfortCount = static_cast(sizeof(comfortNames) / sizeof(comfortNames[0])); std::stringstream ss; ss << "VR Settings:\n"; ss << " Enabled: " << (vr.enabled ? "yes" : "no") << "\n"; ss << " Render Target: " << vr.renderTargetWidth << "x" << vr.renderTargetHeight << "\n"; ss << " Render Scale: " << vr.renderScale << "\n"; - ss << " Tracking: " << trackNames[vr.trackingSpace] << "\n"; + ss << " Tracking: " + << (vr.trackingSpace >= 0 && vr.trackingSpace < trackCount ? trackNames[vr.trackingSpace] + : "Unknown") + << "\n"; ss << " IPD: " << vr.ipd << " m\n"; - ss << " Comfort Mode: " << comfortNames[vr.comfortMode] << "\n"; + ss << " Comfort Mode: " + << (vr.comfortMode >= 0 && vr.comfortMode < comfortCount ? comfortNames[vr.comfortMode] : "Unknown") + << "\n"; ss << " Reprojection: " << (vr.reprojection ? "on" : "off") << "\n"; return ss.str(); }, @@ -613,7 +632,14 @@ namespace Spark auto& vr = EngineSettings::GetInstance().VR(); if (args.empty()) return "VR render scale: " + std::to_string(vr.renderScale); - vr.renderScale = std::clamp(std::stof(args[0]), 0.5f, 2.0f); + try + { + vr.renderScale = std::clamp(std::stof(args[0]), 0.5f, 2.0f); + } + catch (const std::exception&) + { + return "Invalid render scale value. Expected a number between 0.5 and 2.0."; + } return "VR render scale set to " + std::to_string(vr.renderScale); }, "Get/set VR supersampling scale (0.5-2.0)", "VR"); @@ -702,7 +728,14 @@ namespace Spark auto& sv = EngineSettings::GetInstance().SaveSystem(); if (args.empty()) return "Auto-save interval: " + std::to_string(static_cast(sv.autoSaveInterval)) + " s"; - sv.autoSaveInterval = std::clamp(std::stof(args[0]), 0.0f, 3600.0f); + try + { + sv.autoSaveInterval = std::clamp(std::stof(args[0]), 0.0f, 3600.0f); + } + catch (const std::exception&) + { + return "Invalid interval. Expected a number of seconds (0-3600)."; + } return "Auto-save interval set to " + std::to_string(static_cast(sv.autoSaveInterval)) + " s"; }, "Get/set auto-save interval (seconds, 0=disabled)", "SaveSystem"); diff --git a/SparkEngine/Source/Engine/Animation/SkeletalAnimation.cpp b/SparkEngine/Source/Engine/Animation/SkeletalAnimation.cpp index 765ec2b77..ec8d0afd6 100644 --- a/SparkEngine/Source/Engine/Animation/SkeletalAnimation.cpp +++ b/SparkEngine/Source/Engine/Animation/SkeletalAnimation.cpp @@ -149,6 +149,11 @@ namespace Spark::Animation if (boneIdx < 0) continue; } + // A cached channel.boneIndex may outlive the skeleton that produced it + // (retargeting, runtime skeleton swap, corrupted clip data). Bounds-check + // against the current outLocalTransforms size before indexing. + if (static_cast(boneIdx) >= outLocalTransforms.size()) + continue; XMFLOAT3 pos = channel.InterpolatePosition(animTime); XMFLOAT4 rot = channel.InterpolateRotation(animTime); diff --git a/SparkEngine/Source/Physics/ClothSimulation.cpp b/SparkEngine/Source/Physics/ClothSimulation.cpp index 8dff382ac..eafb3eb6d 100644 --- a/SparkEngine/Source/Physics/ClothSimulation.cpp +++ b/SparkEngine/Source/Physics/ClothSimulation.cpp @@ -241,6 +241,10 @@ namespace Spark::Physics void ClothSimulation::IntegratePositions(ClothInstance& cloth, float deltaTime) { + // Skip integration on zero-delta ticks; the divide at the end would produce NaN velocities + // (0 position delta / 0 dt) that persist to downstream consumers. + if (deltaTime <= 1e-8f) + return; float dt2 = deltaTime * deltaTime; float dampFactor = 1.0f - cloth.damping; diff --git a/ThirdParty/SDL2 b/ThirdParty/SDL2 index e4f75bac4..859844eae 160000 --- a/ThirdParty/SDL2 +++ b/ThirdParty/SDL2 @@ -1 +1 @@ -Subproject commit e4f75bac451230d31695158ed35f459e7a04585c +Subproject commit 859844eae358447be8d66e6da59b6fb3df0ed778 From 63c213e7b4a189f1847e287c314536631ee260c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 18:57:47 +0000 Subject: [PATCH 2/9] fix(linux): re-add Vulkan->OpenGL fallback, placed after graphics init The Vulkan->OpenGL fallback block in RunSDL2Windowed was removed in the prior pass because it ran *before* GraphicsEngine was constructed, so GetEngineRuntime().graphics was always null and the branch could never detect a real fallback. Add it back correctly. - Split graphics init out of InitializeSDL2Subsystems into a new InitializeGraphicsForWindow helper. Caller (RunSDL2Windowed) now owns the "construct + Initialize GraphicsEngine" step. - Drop the no-longer-needed nativeRenderHandle parameter from InitializeSDL2Subsystems. - After the first graphics init, if Vulkan was requested but RHIBridge selected a non-Vulkan, non-headless backend (e.g. OpenGL), tear the graphics + SDL window down, rebuild windowFlags with SDL_WINDOW_OPENGL, recreate the window + GL context, and re-run graphics init. The check uses GraphicsEngine::GetRHIDevice()->GetBackendType() and GetRHIBridge()->IsHeadless(), both of which require an initialized graphics engine. Headless and Vulkan-ok paths are unchanged. Build and all 5863 SparkTests still pass. --- SparkEngine/Source/Core/SparkEngineLinux.cpp | 114 ++++++++++++++++--- 1 file changed, 100 insertions(+), 14 deletions(-) diff --git a/SparkEngine/Source/Core/SparkEngineLinux.cpp b/SparkEngine/Source/Core/SparkEngineLinux.cpp index 8e5182e2d..99a8405c3 100644 --- a/SparkEngine/Source/Core/SparkEngineLinux.cpp +++ b/SparkEngine/Source/Core/SparkEngineLinux.cpp @@ -679,22 +679,15 @@ static bool HandleSDLEvent(const SDL_Event& event) } /** - * @brief Initialize SDL2 windowed-mode subsystems: window, graphics, input, - * engine context, modules, audio, and console commands. + * @brief Construct and initialize GraphicsEngine against the given window / + * native render handle. Logs the outcome via SimpleConsole. * - * @param window The SDL2 window (already created by the caller). - * @param argc Argument count from main(). - * @param argv Argument values from main(). + * Extracted so the caller (RunSDL2Windowed) can detect a backend fallback + * (e.g. Vulkan requested → OpenGL selected by RHIBridge) and recreate the + * window + graphics pair with the correct SDL window flag. */ -static void InitializeSDL2Subsystems(SDL_Window* window, void* nativeRenderHandle, int argc, char* argv[]) +static HRESULT InitializeGraphicsForWindow(SDL_Window* window, void* nativeRenderHandle) { - auto& settings = EngineSettings::GetInstance(); - - // Core engine objects - GetEngineRuntime().timer = std::make_unique(); - GetEngineRuntime().eventBus = std::make_unique(); - GetEngineRuntime().input = std::make_unique(); - GetEngineRuntime().input->Initialize(static_cast(window)); GetEngineRuntime().graphics = std::make_unique(); // On macOS+Metal the RHI needs an NSView/CAMetalLayer, not the SDL_Window. @@ -707,6 +700,31 @@ static void InitializeSDL2Subsystems(SDL_Window* window, void* nativeRenderHandl console.LogInfo("Graphics engine initialized (RHI backend)."); else console.LogWarning("Graphics engine initialization deferred (headless fallback)."); + return hr; +} + +/** + * @brief Initialize SDL2 windowed-mode subsystems: input, engine context, + * modules, audio, and console commands. + * + * GraphicsEngine must already be constructed and initialized by the caller. + * The caller owns that step because a Vulkan→OpenGL fallback may require + * recreating the window before graphics init succeeds against the right + * surface type. + * + * @param window The SDL2 window (already created by the caller). + * @param argc Argument count from main(). + * @param argv Argument values from main(). + */ +static void InitializeSDL2Subsystems(SDL_Window* window, int argc, char* argv[]) +{ + auto& settings = EngineSettings::GetInstance(); + + // Core engine objects (graphics is already initialized by the caller) + GetEngineRuntime().timer = std::make_unique(); + GetEngineRuntime().eventBus = std::make_unique(); + GetEngineRuntime().input = std::make_unique(); + GetEngineRuntime().input->Initialize(static_cast(window)); // Engine context, physics, core subsystems, gameplay subsystems InitLinuxCoreSubsystems(/*registerGameplay=*/true); @@ -965,7 +983,75 @@ static int RunSDL2Windowed(int argc, char* argv[]) void* nativeRenderHandle = (preferMetal && sdlMetalView) ? sdlMetalView : nullptr; - InitializeSDL2Subsystems(window, nativeRenderHandle, argc, argv); + // Initialize graphics here (not inside InitializeSDL2Subsystems) so we + // can detect the Vulkan→OpenGL fallback case below. SDL2 bakes the + // backend choice into the window flags at creation time, so if + // RHIBridge falls back from Vulkan to OpenGL we need to recreate the + // window with SDL_WINDOW_OPENGL and re-run graphics init. + InitializeGraphicsForWindow(window, nativeRenderHandle); + + if (preferVulkan) + { + auto* rhiDev = GetEngineRuntime().graphics->GetRHIDevice(); + auto* rhiBridge = GetEngineRuntime().graphics->GetRHIBridge(); + const bool vulkanActive = rhiDev && rhiDev->GetBackendType() == Spark::RHI::GraphicsBackend::Vulkan; + const bool headless = rhiBridge && rhiBridge->IsHeadless(); + + if (!vulkanActive && !headless) + { + SPARK_LOG_WARN(Spark::LogCategory::Graphics, + "Vulkan requested but RHI selected %s — recreating window with SDL_WINDOW_OPENGL", + rhiDev ? Spark::RHI::GetBackendName(rhiDev->GetBackendType()) : ""); + + // Tear down graphics + Vulkan window before rebuilding. + GetEngineRuntime().graphics->Shutdown(); + GetEngineRuntime().graphics.reset(); + SDL_DestroyWindow(window); + SDL_Vulkan_UnloadLibrary(); + preferVulkan = false; + + windowFlags &= ~static_cast(SDL_WINDOW_VULKAN); + windowFlags |= SDL_WINDOW_OPENGL; + + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + + window = SDL_CreateWindow("Spark Engine", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, winW, winH, + windowFlags); + if (!window) + { + Spark::SimpleConsole::GetInstance().LogError(std::string("SDL_CreateWindow (GL fallback) failed: ") + + SDL_GetError()); + SDL_Quit(); + return -1; + } + + glContext = SDL_GL_CreateContext(window); + if (!glContext) + { + Spark::SimpleConsole::GetInstance().LogWarning( + std::string("SDL_GL_CreateContext (GL fallback) failed: ") + SDL_GetError() + + " — engine will try headless fallback"); + } + else + { + SDL_GL_MakeCurrent(window, glContext); + SDL_GL_SetSwapInterval(1); + Spark::SimpleConsole::GetInstance().LogInfo("SDL2 OpenGL context created after Vulkan fallback"); + } + + // Metal view is only valid for a preferMetal path — Vulkan fallback + // never creates one, so nativeRenderHandle stays null here. + InitializeGraphicsForWindow(window, /*nativeRenderHandle=*/nullptr); + } + } + + InitializeSDL2Subsystems(window, argc, argv); RunSDL2MainLoop(); ShutdownLinux(); From 3bfdc8f3fab26a192a9e01abce121b0b26a8c2dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 21:26:54 +0000 Subject: [PATCH 3/9] fix(linux): skip SDL window creation when no GPU backend available Running the engine under gVisor / sandboxed hosts without libEGL.so.1 caused RunSDL2Windowed to exit with -1 after only logging up to "recommended backend = None". Root cause: SDL2's offscreen video driver dlopen()s libEGL during SDL_CreateWindow; on hosts where libEGL is absent the process exits with -1 mid-window-creation without surfacing an error through SDL_GetError(). The `if (!window) return -1;` branch was never even reached. - If RHIBridge::GetRecommendedBackend() returns GraphicsBackend::None (no usable GPU backend), skip SDL_CreateWindow entirely and run the engine windowless on NullRHIDevice. - Demote SDL_CreateWindow failure from fatal (return -1) to a warning; fall through into the same windowless / NullRHIDevice path so the engine comes up instead of exiting cold. - Skip Metal-view / GL-context creation when window is null. - Guard SDL_SetWindowTitle against a null window. End-to-end smoke test: engine now initializes, runs test frames, and shuts down cleanly (exit 0, ~321 log lines) in a libEGL-less gVisor environment where it previously exited after 6 log lines. All 5863 SparkTests still pass. --- SparkEngine/Source/Core/SparkEngineLinux.cpp | 49 +++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/SparkEngine/Source/Core/SparkEngineLinux.cpp b/SparkEngine/Source/Core/SparkEngineLinux.cpp index 99a8405c3..cda4f25e6 100644 --- a/SparkEngine/Source/Core/SparkEngineLinux.cpp +++ b/SparkEngine/Source/Core/SparkEngineLinux.cpp @@ -732,8 +732,9 @@ static void InitializeSDL2Subsystems(SDL_Window* window, int argc, char* argv[]) // Modules, audio, console commands InitLinuxModulesAndCommands(argc, argv, /*initAudio=*/true); - // Update window title with primary module name - if (GetEngineRuntime().moduleManager) + // Update window title with primary module name (only if we have a window — + // the windowless NullRHIDevice fallback path sets window = nullptr). + if (window && GetEngineRuntime().moduleManager) { auto* primary = GetEngineRuntime().moduleManager->GetPrimaryModule(); if (primary) @@ -908,6 +909,20 @@ static int RunSDL2Windowed(int argc, char* argv[]) SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); } + // If RHIBridge already decided there is no usable GPU backend (e.g. under + // gVisor or on a host missing libEGL/libvulkan), skip SDL window creation + // entirely. SDL2's offscreen video driver dlopen()s libEGL during + // SDL_CreateWindow — if libEGL is missing the process exits with -1 + // without surfacing an error through SDL_GetError(). Running windowless + // in that case lets the engine come up on NullRHIDevice. + const bool noGpuBackend = (!preferVulkan && !preferMetal) && + (Spark::RHI::RHIBridge::GetRecommendedBackend() == Spark::RHI::GraphicsBackend::None); + if (noGpuBackend) + { + SPARK_LOG_INFO(Spark::LogCategory::Graphics, + "No GPU backend available — skipping SDL window creation, running on NullRHIDevice"); + } + Uint32 windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; if (settings.Graphics().fullscreen) windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP; @@ -918,15 +933,25 @@ static int RunSDL2Windowed(int argc, char* argv[]) else windowFlags |= SDL_WINDOW_OPENGL; - SDL_Window* window = - SDL_CreateWindow("Spark Engine", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, winW, winH, windowFlags); - if (!window) + SDL_Window* window = nullptr; + if (!noGpuBackend) { - Spark::SimpleConsole::GetInstance().LogError(std::string("SDL_CreateWindow failed: ") + SDL_GetError()); - if (preferVulkan) - SDL_Vulkan_UnloadLibrary(); - SDL_Quit(); - return -1; + window = + SDL_CreateWindow("Spark Engine", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, winW, winH, windowFlags); + if (!window) + { + // Treat window-creation failure as recoverable: the engine will + // initialize graphics against a null handle (NullRHIDevice) and + // run the main loop windowless. This matches the headless + // fallback the rest of the stack already handles. + Spark::SimpleConsole::GetInstance().LogWarning(std::string("SDL_CreateWindow failed: ") + SDL_GetError() + + " — falling back to windowless / NullRHIDevice mode"); + if (preferVulkan) + { + SDL_Vulkan_UnloadLibrary(); + preferVulkan = false; + } + } } // On macOS, extract a Metal-capable view so MetalDevice can attach a @@ -935,7 +960,7 @@ static int RunSDL2Windowed(int argc, char* argv[]) // so MetalSwapChain::ConfigureMetalLayer() reuses it. On non-macOS the // call is a no-op that returns nullptr. void* sdlMetalView = nullptr; - if (preferMetal) + if (preferMetal && window) { sdlMetalView = Spark::MacOS::CreateMetalView(window); if (!sdlMetalView) @@ -952,7 +977,7 @@ static int RunSDL2Windowed(int argc, char* argv[]) // surface out of SDL_Vulkan_CreateSurface() inside CreateSwapChain(). // Metal has no pre-init step either — the Metal view was created above. SDL_GLContext glContext = nullptr; - if (!preferVulkan && !preferMetal) + if (!preferVulkan && !preferMetal && window) { glContext = SDL_GL_CreateContext(window); if (!glContext) From afa015684404753dfb50a493be9e9d40681c4d7e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 21:53:07 +0000 Subject: [PATCH 4/9] fix(linux): demote more init failures from fatal to graceful fallback Following the SDL_CreateWindow guard, apply the same "log warning + run on NullRHIDevice" pattern to three more brittle init paths in RunSDL2Windowed so the engine keeps coming up on constrained hosts instead of exiting with -1. - SDL_Init failure: previously `return -1`. Now logs a warning, sets sdlInitOk=false, forces noGpuBackend=true, and skips all subsequent SDL calls. Vulkan detection, SDL_GetCurrentVideoDriver, GL attribute setup, SDL_Quit, and SDL_Vulkan_UnloadLibrary are all gated on sdlInitOk. ShouldPreferMetal() is short-circuited to false. - RunSDL2MainLoop now takes `pollSdlEvents`; when false (SDL_Init failed) it runs a pure tick loop without SDL_PollEvent so the engine still honors SIGINT / -test-frames without touching uninitialized SDL state. - Metal-view creation failure (macOS): previously `return -1`. Now logs a warning, destroys the Metal window, clears preferMetal, and lets the rest of the function fall through to the windowless / NullRHIDevice path (matching the noGpuBackend branch added earlier). - Vulkan->OpenGL fallback: when the second-chance SDL_CreateWindow call also failed it returned -1. Now logs a warning, keeps window=null, and InitializeGraphicsForWindow(null) will bring up NullRHIDevice. - Cleanup guards: `SDL_DestroyWindow(window)` only runs when window is non-null; SDL_Quit/SDL_Vulkan_UnloadLibrary only run when sdlInitOk. The common theme across all four patches: environmental failures that the rest of the stack can absorb (graphics stack already has a NullRHIDevice fallback, main loop can run without SDL events) should not be fatal. Whole-engine aborts were the blast radius of a missing library or a sandboxed display. All 5863 SparkTests still pass. End-to-end smoke run with SDL_VIDEODRIVER=invalid-driver still produces exit 0 and a clean shutdown. --- SparkEngine/Source/Core/SparkEngineLinux.cpp | 115 ++++++++++++------- 1 file changed, 73 insertions(+), 42 deletions(-) diff --git a/SparkEngine/Source/Core/SparkEngineLinux.cpp b/SparkEngine/Source/Core/SparkEngineLinux.cpp index cda4f25e6..c94bd4302 100644 --- a/SparkEngine/Source/Core/SparkEngineLinux.cpp +++ b/SparkEngine/Source/Core/SparkEngineLinux.cpp @@ -765,11 +765,16 @@ static void InitializeSDL2Subsystems(SDL_Window* window, int argc, char* argv[]) * * Processes SDL events via HandleSDLEvent(), then calls TickFrame() for * the engine update. Returns when the window is closed or SIGINT is received. + * + * @param pollSdlEvents When false (SDL_Init failed), skip event polling and + * run a pure tick loop. The engine still ticks frames and respects + * SIGINT / test-frame-limit, just without SDL event input. */ -static void RunSDL2MainLoop() +static void RunSDL2MainLoop(bool pollSdlEvents) { auto& console = Spark::SimpleConsole::GetInstance(); - console.LogInfo("Starting main engine loop (SDL2)..."); + console.LogInfo(pollSdlEvents ? "Starting main engine loop (SDL2)..." + : "Starting main engine loop (SDL2 uninitialized — tick only)..."); if (g_testFrameLimit > 0) console.LogInfo(std::format("Test mode: will exit after {} frames", g_testFrameLimit)); @@ -784,15 +789,18 @@ static void RunSDL2MainLoop() break; } - SDL_Event event; bool running = true; - while (SDL_PollEvent(&event)) + if (pollSdlEvents) { - if (!HandleSDLEvent(event)) + SDL_Event event; + while (SDL_PollEvent(&event)) { - running = false; - break; + if (!HandleSDLEvent(event)) + { + running = false; + break; + } } } @@ -827,10 +835,16 @@ static int RunSDL2Windowed(int argc, char* argv[]) SDL_SetHint("SDL_VIDEO_X11_FORCE_EGL", "1"); SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1"); - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) + const bool sdlInitOk = (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) == 0); + if (!sdlInitOk) { - Spark::SimpleConsole::GetInstance().LogError(std::string("SDL_Init failed: ") + SDL_GetError()); - return -1; + // SDL_Init can fail on sandboxed hosts with no display server, no + // joystick subsystem, or when dbus/udev aren't reachable. Fall + // through to the windowless / NullRHIDevice path instead of + // aborting — the engine is still useful for running game logic, + // physics, scripting, and networking headlessly. + Spark::SimpleConsole::GetInstance().LogWarning(std::string("SDL_Init failed: ") + SDL_GetError() + + " — falling back to windowless / NullRHIDevice mode"); } auto& settings = EngineSettings::GetInstance(); @@ -850,7 +864,8 @@ static int RunSDL2Windowed(int argc, char* argv[]) // ICD can set SPARK_DISABLE_VULKAN=1 and this branch will pick // OpenGL instead — matching what the RHIBridge will also choose. bool preferVulkan = false; - bool preferMetal = Spark::MacOS::ShouldPreferMetal(); + bool preferMetal = sdlInitOk && Spark::MacOS::ShouldPreferMetal(); + if (sdlInitOk) { const char* sdlDriver = SDL_GetCurrentVideoDriver(); SPARK_LOG_INFO(Spark::LogCategory::Graphics, "SDL2 video driver: %s", sdlDriver ? sdlDriver : ""); @@ -896,7 +911,7 @@ static int RunSDL2Windowed(int argc, char* argv[]) } } - if (!preferVulkan && !preferMetal) + if (sdlInitOk && !preferVulkan && !preferMetal) { // OpenGL path — set attributes before window creation (required // for Mesa llvmpipe and other software rasterizers). @@ -909,14 +924,16 @@ static int RunSDL2Windowed(int argc, char* argv[]) SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); } - // If RHIBridge already decided there is no usable GPU backend (e.g. under - // gVisor or on a host missing libEGL/libvulkan), skip SDL window creation - // entirely. SDL2's offscreen video driver dlopen()s libEGL during - // SDL_CreateWindow — if libEGL is missing the process exits with -1 - // without surfacing an error through SDL_GetError(). Running windowless - // in that case lets the engine come up on NullRHIDevice. - const bool noGpuBackend = (!preferVulkan && !preferMetal) && - (Spark::RHI::RHIBridge::GetRecommendedBackend() == Spark::RHI::GraphicsBackend::None); + // If SDL itself failed to initialize, or RHIBridge already decided there + // is no usable GPU backend (e.g. under gVisor or on a host missing + // libEGL/libvulkan), skip SDL window creation entirely. SDL2's offscreen + // video driver dlopen()s libEGL during SDL_CreateWindow — if libEGL is + // missing the process exits with -1 without surfacing an error through + // SDL_GetError(). Running windowless in that case lets the engine come up + // on NullRHIDevice. + const bool noGpuBackend = + !sdlInitOk || ((!preferVulkan && !preferMetal) && + (Spark::RHI::RHIBridge::GetRecommendedBackend() == Spark::RHI::GraphicsBackend::None)); if (noGpuBackend) { SPARK_LOG_INFO(Spark::LogCategory::Graphics, @@ -965,9 +982,15 @@ static int RunSDL2Windowed(int argc, char* argv[]) sdlMetalView = Spark::MacOS::CreateMetalView(window); if (!sdlMetalView) { + // Metal framework unavailable / sandbox restriction. Tear down + // the Metal window and continue windowless — RHIBridge will + // select NullRHIDevice and the engine will run headlessly rather + // than aborting. + Spark::SimpleConsole::GetInstance().LogWarning( + "Spark::MacOS::CreateMetalView returned null — running windowless on NullRHIDevice"); SDL_DestroyWindow(window); - SDL_Quit(); - return -1; + window = nullptr; + preferMetal = false; } } @@ -1050,24 +1073,28 @@ static int RunSDL2Windowed(int argc, char* argv[]) windowFlags); if (!window) { - Spark::SimpleConsole::GetInstance().LogError(std::string("SDL_CreateWindow (GL fallback) failed: ") + - SDL_GetError()); - SDL_Quit(); - return -1; - } - - glContext = SDL_GL_CreateContext(window); - if (!glContext) - { - Spark::SimpleConsole::GetInstance().LogWarning( - std::string("SDL_GL_CreateContext (GL fallback) failed: ") + SDL_GetError() + - " — engine will try headless fallback"); + // Second-chance GL window creation also failed. Don't abort; + // initialize graphics against a null handle and run the engine + // on NullRHIDevice, matching the noGpuBackend path above. + Spark::SimpleConsole::GetInstance().LogWarning(std::string("SDL_CreateWindow (GL fallback) failed: ") + + SDL_GetError() + + " — falling back to windowless / NullRHIDevice mode"); } else { - SDL_GL_MakeCurrent(window, glContext); - SDL_GL_SetSwapInterval(1); - Spark::SimpleConsole::GetInstance().LogInfo("SDL2 OpenGL context created after Vulkan fallback"); + glContext = SDL_GL_CreateContext(window); + if (!glContext) + { + Spark::SimpleConsole::GetInstance().LogWarning( + std::string("SDL_GL_CreateContext (GL fallback) failed: ") + SDL_GetError() + + " — engine will try headless fallback"); + } + else + { + SDL_GL_MakeCurrent(window, glContext); + SDL_GL_SetSwapInterval(1); + Spark::SimpleConsole::GetInstance().LogInfo("SDL2 OpenGL context created after Vulkan fallback"); + } } // Metal view is only valid for a preferMetal path — Vulkan fallback @@ -1077,16 +1104,20 @@ static int RunSDL2Windowed(int argc, char* argv[]) } InitializeSDL2Subsystems(window, argc, argv); - RunSDL2MainLoop(); + RunSDL2MainLoop(/*pollSdlEvents=*/sdlInitOk); ShutdownLinux(); if (glContext) SDL_GL_DeleteContext(glContext); Spark::MacOS::DestroyMetalView(sdlMetalView); - SDL_DestroyWindow(window); - if (preferVulkan) - SDL_Vulkan_UnloadLibrary(); - SDL_Quit(); + if (window) + SDL_DestroyWindow(window); + if (sdlInitOk) + { + if (preferVulkan) + SDL_Vulkan_UnloadLibrary(); + SDL_Quit(); + } return 0; } #endif // SPARK_SDL2_AVAILABLE From 36656d0fc05cfd7203be5c3367264a2bdd1f52b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 22:45:59 +0000 Subject: [PATCH 5/9] fix(windows): resolve three VS2022 CI build failures 1. ProjectBrowserPanel.cpp: add (for CSIDL_PERSONAL and SHGetFolderPathA) and under _WIN32. 2. PostProcessingPipeline.cpp: the six D3D11 functions (CreatePingPongTargets, CreatePostProcessShaders, CompileEffectShaders, BeginPass, DrawFullscreen, ProcessPass) have canonical implementations in PostProcessingPipelineWindows.cpp. Guard the stub copies here with #ifndef SPARK_PLATFORM_WINDOWS so we don't emit LNK4006 duplicate-definition warnings on Windows while still providing stubs on Linux/macOS. 3. SparkInstaller/CMakeLists.txt: switch MSVC_RUNTIME_LIBRARY from MultiThreaded[Debug] (static /MT[d]) to MultiThreaded[Debug]DLL (/MD[d]) to match the imgui target (cmake/BuildImGui.cmake). The CRT mismatch was producing LNK2019 unresolved __imp_* symbols when linking imgui.lib. --- SparkEditor/Source/Panels/ProjectBrowserPanel.cpp | 5 +++++ SparkEngine/Source/Graphics/PostProcessingPipeline.cpp | 8 ++++++++ SparkInstaller/CMakeLists.txt | 4 +++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/SparkEditor/Source/Panels/ProjectBrowserPanel.cpp b/SparkEditor/Source/Panels/ProjectBrowserPanel.cpp index e31fa521d..cd1711626 100644 --- a/SparkEditor/Source/Panels/ProjectBrowserPanel.cpp +++ b/SparkEditor/Source/Panels/ProjectBrowserPanel.cpp @@ -17,6 +17,11 @@ #include #include +#ifdef _WIN32 +#include +#include +#endif + namespace fs = std::filesystem; namespace SparkEditor diff --git a/SparkEngine/Source/Graphics/PostProcessingPipeline.cpp b/SparkEngine/Source/Graphics/PostProcessingPipeline.cpp index 0c0474ab8..2b7b74391 100644 --- a/SparkEngine/Source/Graphics/PostProcessingPipeline.cpp +++ b/SparkEngine/Source/Graphics/PostProcessingPipeline.cpp @@ -467,6 +467,12 @@ namespace Spark::Graphics // GPU Resource Creation // ============================================================================= +// Windows D3D11 implementations of the six functions below (CreatePingPongTargets, +// CreatePostProcessShaders, CompileEffectShaders, BeginPass, DrawFullscreen, +// ProcessPass) live in PostProcessingPipelineWindows.cpp. Emitting them here on +// Windows too would produce LNK4006 duplicate-definition warnings. +#ifndef SPARK_PLATFORM_WINDOWS + bool PostProcessingPipeline::CreatePingPongTargets() { #ifdef SPARK_PLATFORM_WINDOWS @@ -1527,4 +1533,6 @@ namespace Spark::Graphics m_passTimings[static_cast(pass)] = ms; } +#endif // !SPARK_PLATFORM_WINDOWS + } // namespace Spark::Graphics diff --git a/SparkInstaller/CMakeLists.txt b/SparkInstaller/CMakeLists.txt index 8f9f24c17..f8f93571e 100644 --- a/SparkInstaller/CMakeLists.txt +++ b/SparkInstaller/CMakeLists.txt @@ -84,8 +84,10 @@ if(INSTALLER_GUI_AVAILABLE) endif() if(MSVC) + # Match imgui's CRT linkage (cmake/BuildImGui.cmake uses the DLL CRT). + # Mismatched /MT vs /MD causes LNK2019 on __imp_* symbols from imgui.lib. set_property(TARGET SparkInstaller PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") target_compile_definitions(SparkInstaller PRIVATE _CRT_SECURE_NO_WARNINGS NOMINMAX From 2b07f24972b9d8c8db9758e3e439e9a67ab34f89 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 23:11:03 +0000 Subject: [PATCH 6/9] deps: pin SDL2 submodule to libsdl-org/SDL 'SDL2' branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The libsdl-org/SDL repo's default branch is now SDL3 development. When .gitmodules didn't specify a tracked branch, Dependabot's submodule updater (PR #485, commit 9a0202e) saw "newer" commits on the main/SDL3 branch and bumped ThirdParty/SDL2 from 859844e (SDL 2.30.0) to e4f75ba (SDL 3.5.0 — "Remove SDL_gtk"). That silently broke every fresh clone: SDL3 uses include/SDL3/ (not include/SDL.h) and exposes SDL3/SDL3::SDL3 CMake targets, so the engine's SDL2 target check failed → SparkEditor skipped → SparkLauncher's find_package(SDL2 REQUIRED) hard-errored. The submodule pointer itself was already restored to 859844e on this branch in commit 54ba527. Adding branch = SDL2 here tells Dependabot to only propose bumps from the SDL2 stable branch, preventing future SDL2→SDL3 "bumps". Verified end-to-end: SparkInstaller clone + cmake configure on a fresh tree — SDL2 audit OK, SparkEditor added, SparkLauncher finds SDL2. --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index 97738494e..b34db1d72 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,4 @@ [submodule "ThirdParty/SDL2"] path = ThirdParty/SDL2 url = https://github.com/libsdl-org/SDL.git + branch = SDL2 From 8eaa659ab1d9c04e2f426ba2ee673a2539d29cd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 23:16:26 +0000 Subject: [PATCH 7/9] style: apply clang-format to editor files Fixes CI check-format failures in HierarchyPanel.cpp, EditorFonts.cpp, and EditorTheme.cpp. --- SparkEditor/Source/Core/EditorFonts.cpp | 7 ++--- SparkEditor/Source/Core/EditorTheme.cpp | 28 ++++++++++---------- SparkEditor/Source/Panels/HierarchyPanel.cpp | 7 +++-- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/SparkEditor/Source/Core/EditorFonts.cpp b/SparkEditor/Source/Core/EditorFonts.cpp index 4dc7bbfab..aa092ed99 100644 --- a/SparkEditor/Source/Core/EditorFonts.cpp +++ b/SparkEditor/Source/Core/EditorFonts.cpp @@ -53,11 +53,8 @@ namespace SparkEditor #endif const char* fontPaths[] = { - exeFontsPath.c_str(), // next to binary (works regardless of CWD) - "EditorAssets/Fonts/", - "Fonts/", - "../SparkEditor/Fonts/", - "../Fonts/", + exeFontsPath.c_str(), // next to binary (works regardless of CWD) + "EditorAssets/Fonts/", "Fonts/", "../SparkEditor/Fonts/", "../Fonts/", }; std::string fontDir; diff --git a/SparkEditor/Source/Core/EditorTheme.cpp b/SparkEditor/Source/Core/EditorTheme.cpp index 9d5cbe507..5bc788d15 100644 --- a/SparkEditor/Source/Core/EditorTheme.cpp +++ b/SparkEditor/Source/Core/EditorTheme.cpp @@ -593,23 +593,23 @@ namespace SparkEditor t.author = "Spark Engine Team"; // Warm neutral background stack (hue ≈ 60, very low chroma). - t.background = ThemeColor::FromHex("#1A1716"); // bg-1: panel - t.backgroundDark = ThemeColor::FromHex("#110E0D"); // bg-0: deepest - t.backgroundLight = ThemeColor::FromHex("#302D2A"); // bg-3: hover/input + t.background = ThemeColor::FromHex("#1A1716"); // bg-1: panel + t.backgroundDark = ThemeColor::FromHex("#110E0D"); // bg-0: deepest + t.backgroundLight = ThemeColor::FromHex("#302D2A"); // bg-3: hover/input t.backgroundAccent = ThemeColor::FromHex("#AF530D").WithAlpha(0.22f); - t.backgroundHeader = ThemeColor::FromHex("#23201E"); // bg-2: header row + t.backgroundHeader = ThemeColor::FromHex("#23201E"); // bg-2: header row t.backgroundActive = ThemeColor::FromHex("#F1823A").WithAlpha(0.20f); t.backgroundHover = ThemeColor::FromHex("#302D2A"); t.backgroundSelected = ThemeColor::FromHex("#F1823A").WithAlpha(0.16f); // Warm off-white foreground (hue ≈ 80). - t.text = ThemeColor::FromHex("#C5C3C1"); // fg-1 - t.textDisabled = ThemeColor::FromHex("#64625F"); // fg-3 - t.textSecondary = ThemeColor::FromHex("#8D8B88"); // fg-2 - t.textAccent = ThemeColor::FromHex("#F1823A"); // ember - t.textWarning = ThemeColor::FromHex("#EDCF59"); // yellow - t.textError = ThemeColor::FromHex("#FA6862"); // red - t.textSuccess = ThemeColor::FromHex("#6ED086"); // green + t.text = ThemeColor::FromHex("#C5C3C1"); // fg-1 + t.textDisabled = ThemeColor::FromHex("#64625F"); // fg-3 + t.textSecondary = ThemeColor::FromHex("#8D8B88"); // fg-2 + t.textAccent = ThemeColor::FromHex("#F1823A"); // ember + t.textWarning = ThemeColor::FromHex("#EDCF59"); // yellow + t.textError = ThemeColor::FromHex("#FA6862"); // red + t.textSuccess = ThemeColor::FromHex("#6ED086"); // green // Buttons — slightly raised from panel. t.button = ThemeColor::FromHex("#23201E"); @@ -623,10 +623,10 @@ namespace SparkEditor t.frameActive = ThemeColor::FromHex("#F1823A").WithAlpha(0.35f); // Borders — barely visible on idle, ember on focus. - t.border = ThemeColor::FromHex("#35322F"); // line - t.borderLight = ThemeColor::FromHex("#403C38"); // bg-4 + t.border = ThemeColor::FromHex("#35322F"); // line + t.borderLight = ThemeColor::FromHex("#403C38"); // bg-4 t.borderAccent = ThemeColor::FromHex("#F1823A"); - t.borderSeparator = ThemeColor::FromHex("#282523"); // line-soft + t.borderSeparator = ThemeColor::FromHex("#282523"); // line-soft // Title bar — deepest charcoal; active tab uses a dim ember wash. t.titleBar = ThemeColor::FromHex("#110E0D"); diff --git a/SparkEditor/Source/Panels/HierarchyPanel.cpp b/SparkEditor/Source/Panels/HierarchyPanel.cpp index 5c8e90826..f13b012e2 100644 --- a/SparkEditor/Source/Panels/HierarchyPanel.cpp +++ b/SparkEditor/Source/Panels/HierarchyPanel.cpp @@ -485,10 +485,9 @@ namespace SparkEditor const ImVec2 itemMin = ImGui::GetItemRectMin(); const ImVec2 itemMax = ImGui::GetItemRectMax(); const ImVec4 accent = ImGui::GetStyleColorVec4(ImGuiCol_NavHighlight); - ImGui::GetWindowDrawList()->AddRectFilled( - ImVec2(ImGui::GetWindowPos().x, itemMin.y), - ImVec2(ImGui::GetWindowPos().x + 2.0f, itemMax.y), - ImGui::GetColorU32(accent)); + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(ImGui::GetWindowPos().x, itemMin.y), + ImVec2(ImGui::GetWindowPos().x + 2.0f, itemMax.y), + ImGui::GetColorU32(accent)); } if (!object->active) From 701226195f78ee1e2eca2c1a646e83f3122eff19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 23:24:28 +0000 Subject: [PATCH 8/9] fix(cmake): copy SDL2 headers at configure time; split Linux/macOS SDL options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes two CI failures: 1. Linux GCC/Clang: "fatal error: SDL_main.h: No such file or directory" at build/ThirdParty/SDL2/include/SDL2/SDL.h. SDL2 copies its public headers to the build tree via a build-time custom_target (sdl_headers_copy). Consumers that pick up SDL2's INTERFACE_INCLUDE_DIRECTORIES without a direct build-order dep (cached restores, tests reaching SDL.h through SparkEngineLib's PUBLIC include path) can race the copy. Copy the headers eagerly at configure time with configure_file COPYONLY so they are present before any compilation runs. 2. macOS: missing cocoa .m object files at link time (SDL_cocoaevents.m.o, SDL_cocoametalview.m.o, etc.). The root project declared only LANGUAGES CXX C, so CMake silently skipped SDL2's Objective-C sources. Enable OBJC and OBJCXX in project() on Apple platforms so both SDL2's cocoa/.m files and MetalDevice.mm compile. Also stop forcing SDL_X11=ON on macOS — X11 is irrelevant there, and setting it on caused SDL2's configure to probe for X11 headers that don't exist on the macOS runner. --- CMakeLists.txt | 67 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 24121702c..2cb5d2f86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,16 @@ if(APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET) endif() set(SPARK_ENGINE_VERSION "1.0.0" CACHE STRING "SparkEngine semantic version used for package metadata") -project(SparkEngine VERSION ${SPARK_ENGINE_VERSION} LANGUAGES CXX C) + +# Objective-C / Objective-C++ are required on macOS so SDL2's cocoa/.m sources +# (SDL_cocoaevents.m, SDL_cocoametalview.m, SDL_rwopsbundlesupport.m, etc.) and +# our own MetalDevice.mm can compile. Without them CMake silently skips .m/.mm +# sources and the link fails with "no such file or directory" on the .m.o files. +if(APPLE) + project(SparkEngine VERSION ${SPARK_ENGINE_VERSION} LANGUAGES CXX C OBJC OBJCXX) +else() + project(SparkEngine VERSION ${SPARK_ENGINE_VERSION} LANGUAGES CXX C) +endif() # Set consistent runtime library for all targets using modern CMake (CMP0091) # Must be after project() so that the MSVC variable is defined. @@ -519,20 +528,56 @@ if(NOT WIN32 AND ENABLE_SDL2 AND EXISTS "${SDL2_SUBMODULE_DIR}/CMakeLists.txt") set(SDL_SHARED ON CACHE BOOL "" FORCE) set(SDL_STATIC OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) - set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) - set(SDL_KMSDRM OFF CACHE BOOL "" FORCE) - set(SDL_DBUS OFF CACHE BOOL "" FORCE) - set(SDL_IBUS OFF CACHE BOOL "" FORCE) - set(SDL_PULSEAUDIO OFF CACHE BOOL "" FORCE) - set(SDL_ALSA OFF CACHE BOOL "" FORCE) - set(SDL_PIPEWIRE OFF CACHE BOOL "" FORCE) - set(SDL_SNDIO OFF CACHE BOOL "" FORCE) - set(SDL_JACK OFF CACHE BOOL "" FORCE) - set(SDL_X11 ON CACHE BOOL "" FORCE) set(SDL_OPENGL ON CACHE BOOL "" FORCE) + if(APPLE) + # macOS: Cocoa + CoreAudio. X11/Wayland/ALSA etc. are irrelevant. + set(SDL_X11 OFF CACHE BOOL "" FORCE) + set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) + set(SDL_KMSDRM OFF CACHE BOOL "" FORCE) + set(SDL_DBUS OFF CACHE BOOL "" FORCE) + set(SDL_IBUS OFF CACHE BOOL "" FORCE) + set(SDL_PULSEAUDIO OFF CACHE BOOL "" FORCE) + set(SDL_ALSA OFF CACHE BOOL "" FORCE) + set(SDL_PIPEWIRE OFF CACHE BOOL "" FORCE) + set(SDL_SNDIO OFF CACHE BOOL "" FORCE) + set(SDL_JACK OFF CACHE BOOL "" FORCE) + else() + # Linux: X11 + OpenGL, everything else off to keep the build lean. + set(SDL_X11 ON CACHE BOOL "" FORCE) + set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) + set(SDL_KMSDRM OFF CACHE BOOL "" FORCE) + set(SDL_DBUS OFF CACHE BOOL "" FORCE) + set(SDL_IBUS OFF CACHE BOOL "" FORCE) + set(SDL_PULSEAUDIO OFF CACHE BOOL "" FORCE) + set(SDL_ALSA OFF CACHE BOOL "" FORCE) + set(SDL_PIPEWIRE OFF CACHE BOOL "" FORCE) + set(SDL_SNDIO OFF CACHE BOOL "" FORCE) + set(SDL_JACK OFF CACHE BOOL "" FORCE) + endif() + add_subdirectory("${SDL2_SUBMODULE_DIR}" "${CMAKE_BINARY_DIR}/ThirdParty/SDL2" EXCLUDE_FROM_ALL) set(SDL2_FOUND TRUE) + + # Eagerly copy SDL2's public headers to the build tree at configure time. + # SDL2 normally copies them via a build-time custom_target ("sdl_headers_copy"), + # but consumers that pick up SDL2's INTERFACE_INCLUDE_DIRECTORIES without a + # direct build-order dep on SDL2 (e.g. cached CI restores, tests using + # through SparkEngineLib's PUBLIC include path) can race the copy and fail + # with "fatal error: SDL_main.h: No such file or directory" at + # build/ThirdParty/SDL2/include/SDL2/SDL.h. Copying at configure time makes + # the headers available before any compilation job runs. + file(GLOB _SDL2_PUBLIC_HEADERS "${SDL2_SUBMODULE_DIR}/include/*.h") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/ThirdParty/SDL2/include/SDL2") + foreach(_hdr IN LISTS _SDL2_PUBLIC_HEADERS) + if(NOT _hdr MATCHES ".*(SDL_config|SDL_revision).*") + get_filename_component(_name "${_hdr}" NAME) + configure_file("${_hdr}" + "${CMAKE_BINARY_DIR}/ThirdParty/SDL2/include/SDL2/${_name}" + COPYONLY) + endif() + endforeach() + message(STATUS "SDL2 built from submodule (ThirdParty/SDL2)") endif() From a40ac4aa1d762173b6fafe3ec192fdf8930c7b60 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 00:28:55 +0000 Subject: [PATCH 9/9] fix: macOS OpenAL + MetalDevice + SparkBuild CRT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the remaining CI failures surfaced after the SDL2 eager-copy fix got Linux green: - SparkEngine/Source/Audio/OpenALAudioEngine.cpp: include on macOS when available. CMake's FindOpenAL picks the system OpenAL.framework, whose headers live at OpenAL/al.h (not AL/al.h). Homebrew's openal-soft still uses AL/al.h, so fall back to that form everywhere else. - SparkEngine/Source/Graphics/RHI/Metal/MetalDevice.mm: include Utils/LogMacros.h (SPARK_LOG_ERROR lives there, not in Logger.h), and replace ++m_statistics.frameCount with ResetStatistics() to match the other RHI backends — RHIStatistics has no frameCount field. - SparkBuild/CMakeLists.txt: drop the per-target /MT[d] override on SparkBuildCore and SparkBuild. The top-level project sets CMAKE_MSVC_RUNTIME_LIBRARY to /MD[d]; forcing /MT[d] here made SparkInstaller (which links both SparkBuildCore and imgui, the latter built /MD[d]) pull in both static and dynamic CRT variants and fail with LNK1169 on VS2022 Debug. - ThirdParty/dependencies.lock: bump Last sync to 2026-04-20 so the check-thirdparty-manifest job accepts the SDL2 .gitmodules / CMakeLists.txt changes. --- SparkBuild/CMakeLists.txt | 13 +++++-------- SparkEngine/Source/Audio/OpenALAudioEngine.cpp | 8 ++++++++ .../Source/Graphics/RHI/Metal/MetalDevice.mm | 3 ++- ThirdParty/dependencies.lock | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/SparkBuild/CMakeLists.txt b/SparkBuild/CMakeLists.txt index 3b3df88e7..d4c827485 100644 --- a/SparkBuild/CMakeLists.txt +++ b/SparkBuild/CMakeLists.txt @@ -50,10 +50,10 @@ if(WIN32) ) endif() -if(MSVC) - set_property(TARGET SparkBuildCore PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -endif() +# Inherit the top-level /MD[d] CRT (CMAKE_MSVC_RUNTIME_LIBRARY is set by the +# engine's root CMakeLists). Forcing /MT[d] here causes SparkInstaller — which +# also links imgui (built with /MD[d] via BuildImGui.cmake) — to pull in both +# CRT variants and fail with LNK1169 / LNK2005 on duplicate symbols. if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_link_libraries(SparkBuildCore PUBLIC pthread) @@ -77,10 +77,7 @@ endif() target_link_libraries(SparkBuild PRIVATE SparkBuildCore) -if(MSVC) - set_property(TARGET SparkBuild PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -endif() +# Inherit the top-level /MD[d] CRT — see comment above SparkBuildCore. if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(SparkBuild PRIVATE -Wall -Wextra -Wpedantic) diff --git a/SparkEngine/Source/Audio/OpenALAudioEngine.cpp b/SparkEngine/Source/Audio/OpenALAudioEngine.cpp index 76bdc9f32..a32948ee9 100644 --- a/SparkEngine/Source/Audio/OpenALAudioEngine.cpp +++ b/SparkEngine/Source/Audio/OpenALAudioEngine.cpp @@ -15,8 +15,16 @@ #include "../Utils/Validate.h" #ifdef SPARK_OPENAL_AVAILABLE +// On macOS the system OpenAL.framework lays headers out as . +// openal-soft (Homebrew, Linux) uses the form. Prefer the +// framework style on Apple so the build works with either provider. +#if defined(__APPLE__) && __has_include() +#include +#include +#else #include #include +#endif #else // Stub typedefs when OpenAL is not available - all operations become no-ops typedef int ALuint; diff --git a/SparkEngine/Source/Graphics/RHI/Metal/MetalDevice.mm b/SparkEngine/Source/Graphics/RHI/Metal/MetalDevice.mm index e790500f7..5cfe58a2d 100644 --- a/SparkEngine/Source/Graphics/RHI/Metal/MetalDevice.mm +++ b/SparkEngine/Source/Graphics/RHI/Metal/MetalDevice.mm @@ -8,6 +8,7 @@ #include "MetalDevice.h" #include "../../../Utils/Logger.h" +#include "../../../Utils/LogMacros.h" #import #import @@ -1224,7 +1225,7 @@ void MetalDevice::BeginFrame() { - ++m_statistics.frameCount; + ResetStatistics(); } void MetalDevice::EndFrame() diff --git a/ThirdParty/dependencies.lock b/ThirdParty/dependencies.lock index 138d62c38..b85608cbe 100644 --- a/ThirdParty/dependencies.lock +++ b/ThirdParty/dependencies.lock @@ -2,7 +2,7 @@ # Format: # "name|source|version_or_commit|license|local_path|required_files_csv|feature_macro|fallback_or_stub_path|severity" # severity: ERROR (configure-time fatal), WARN (configure-time warning) -# Last sync: 2026-04-16 (SparkDaemon subproject added: `add_subdirectory(SparkDaemon)` in top-level CMakeLists.txt introduces the new first-party SparkDaemon executable + `SparkEngine/Source/Utils/DaemonLifecycle*` engine wiring. No third-party wiring, no vendored deps, no submodule pointer changes — the manifest-sync regex trips on `CMAKE_SOURCE_DIR` ("_DIR" token) inside the new subproject gate. Previously — 2026-04-12 Platform split: CMakeLists.txt SPARK_ENGINE_ENTRY_POINT changed to SPARK_ENGINE_ENTRY_POINTS list for SparkEngine.cpp/Windows/Linux three-file split — no third-party wiring or vendored dependency changes.) +# Last sync: 2026-04-20 (SDL2 branch pin + CMake split: `.gitmodules` adds `branch = SDL2` hint for libsdl-org/SDL so Dependabot stays on the SDL2 line rather than auto-bumping to SDL3, and CMakeLists.txt SDL2 config block now splits Linux/macOS options (X11 off on Apple, Objective-C/Objective-C++ enabled via project() so SDL2's cocoa .m sources compile) and copies SDL2's public headers at configure time to avoid a build-order race with the `sdl_headers_copy` target. Submodule pointer unchanged (859844eae, SDL 2.30.0). Previously — 2026-04-16 (SparkDaemon subproject added: `add_subdirectory(SparkDaemon)` in top-level CMakeLists.txt introduces the new first-party SparkDaemon executable + `SparkEngine/Source/Utils/DaemonLifecycle*` engine wiring. No third-party wiring, no vendored deps, no submodule pointer changes — the manifest-sync regex trips on `CMAKE_SOURCE_DIR` ("_DIR" token) inside the new subproject gate. Previously — 2026-04-12 Platform split: CMakeLists.txt SPARK_ENGINE_ENTRY_POINT changed to SPARK_ENGINE_ENTRY_POINTS list for SparkEngine.cpp/Windows/Linux three-file split — no third-party wiring or vendored dependency changes.) set(SPARK_THIRDPARTY_AUDIT_ENTRIES "Jolt Physics|https://github.com/jrouwe/JoltPhysics|v5.5.1 (vendored snapshot)|MIT|ThirdParty/Physics/JoltPhysics|Jolt/Jolt.h,Build/CMakeLists.txt|SPARK_JOLT_PHYSICS_AVAILABLE|SparkEngine/Source/Physics/PhysicsSystemStub.cpp|ERROR"