#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "app/trace.hpp" #include "app/sdl3_app.hpp" #include #include "logging/logger.hpp" namespace sdl3cpp::app { std::atomic g_signalReceived{false}; } namespace { void SignalHandler(int signal) { if (signal == SIGINT || signal == SIGTERM) { sdl3cpp::app::g_signalReceived.store(true); } } void SetupSignalHandlers() { std::signal(SIGINT, SignalHandler); std::signal(SIGTERM, SignalHandler); } std::filesystem::path FindScriptPath(const char* argv0) { std::filesystem::path executable; if (argv0 && *argv0 != '\0') { executable = std::filesystem::path(argv0); if (executable.is_relative()) { executable = std::filesystem::current_path() / executable; } } else { executable = std::filesystem::current_path(); } executable = std::filesystem::weakly_canonical(executable); std::filesystem::path scriptPath = executable.parent_path() / "scripts" / "cube_logic.lua"; if (!std::filesystem::exists(scriptPath)) { throw std::runtime_error("Could not find Lua script at " + scriptPath.string()); } return scriptPath; } struct RuntimeConfig { uint32_t width = sdl3cpp::app::kWidth; uint32_t height = sdl3cpp::app::kHeight; std::filesystem::path scriptPath; bool luaDebug = false; }; RuntimeConfig GenerateDefaultRuntimeConfig(const char* argv0) { RuntimeConfig config; config.scriptPath = FindScriptPath(argv0); return config; } RuntimeConfig LoadRuntimeConfigFromJson(const std::filesystem::path& configPath, bool dumpConfig) { std::ifstream configStream(configPath); if (!configStream) { throw std::runtime_error("Failed to open config file: " + configPath.string()); } rapidjson::IStreamWrapper inputWrapper(configStream); rapidjson::Document document; document.ParseStream(inputWrapper); if (document.HasParseError()) { throw std::runtime_error("Failed to parse JSON config at " + configPath.string()); } if (!document.IsObject()) { throw std::runtime_error("JSON config must contain an object at the root"); } if (dumpConfig) { rapidjson::StringBuffer buffer; rapidjson::PrettyWriter writer(buffer); writer.SetIndent(' ', 2); document.Accept(writer); std::cout << "Loaded runtime config (" << configPath << "):\n" << buffer.GetString() << '\n'; } const char* scriptField = "lua_script"; if (!document.HasMember(scriptField) || !document[scriptField].IsString()) { throw std::runtime_error("JSON config requires a string member '" + std::string(scriptField) + "'"); } std::optional projectRoot; const char* projectRootField = "project_root"; if (document.HasMember(projectRootField) && document[projectRootField].IsString()) { std::filesystem::path candidate(document[projectRootField].GetString()); if (candidate.is_absolute()) { projectRoot = std::filesystem::weakly_canonical(candidate); } else { projectRoot = std::filesystem::weakly_canonical(configPath.parent_path() / candidate); } } RuntimeConfig config; const auto& scriptValue = document[scriptField]; std::filesystem::path scriptPath(scriptValue.GetString()); if (!scriptPath.is_absolute()) { if (projectRoot) { scriptPath = *projectRoot / scriptPath; } else { scriptPath = configPath.parent_path() / scriptPath; } } scriptPath = std::filesystem::weakly_canonical(scriptPath); if (!std::filesystem::exists(scriptPath)) { throw std::runtime_error("Lua script not found at " + scriptPath.string()); } config.scriptPath = scriptPath; auto parseDimension = [&](const char* name, uint32_t defaultValue) -> uint32_t { if (!document.HasMember(name)) { return defaultValue; } const auto& value = document[name]; if (value.IsUint()) { return value.GetUint(); } if (value.IsInt()) { int maybeValue = value.GetInt(); if (maybeValue >= 0) { return static_cast(maybeValue); } } throw std::runtime_error(std::string("JSON member '") + name + "' must be a non-negative integer"); }; config.width = parseDimension("window_width", config.width); config.height = parseDimension("window_height", config.height); if (document.HasMember("lua_debug")) { const auto& value = document["lua_debug"]; if (!value.IsBool()) { throw std::runtime_error("JSON member 'lua_debug' must be a boolean"); } config.luaDebug = value.GetBool(); } return config; } std::optional GetUserConfigDirectory() { #ifdef _WIN32 if (const char* appData = std::getenv("APPDATA")) { return std::filesystem::path(appData) / "sdl3cpp"; } #else if (const char* xdgConfig = std::getenv("XDG_CONFIG_HOME")) { return std::filesystem::path(xdgConfig) / "sdl3cpp"; } if (const char* home = std::getenv("HOME")) { return std::filesystem::path(home) / ".config" / "sdl3cpp"; } #endif return std::nullopt; } std::optional GetDefaultConfigPath() { if (auto dir = GetUserConfigDirectory()) { return *dir / "default_runtime.json"; } return std::nullopt; } struct AppOptions { RuntimeConfig runtimeConfig; std::optional seedOutput; bool saveDefaultJson = false; bool dumpRuntimeJson = false; bool traceEnabled = false; }; AppOptions ParseCommandLine(int argc, char** argv) { std::string jsonInputText; std::string seedOutputText; std::string setDefaultJsonPath; bool dumpRuntimeJson = false; bool traceRuntime = false; CLI::App app("SDL3 + Vulkan runtime helper"); app.add_option("-j,--json-file-in", jsonInputText, "Path to a runtime JSON config") ->check(CLI::ExistingFile); app.add_option("-s,--create-seed-json", seedOutputText, "Write a template runtime JSON file"); auto* setDefaultJsonOption = app.add_option( "-d,--set-default-json", setDefaultJsonPath, "Persist the runtime JSON to the platform default location (XDG/APPDATA); " "provide PATH to copy that JSON instead of using the default contents"); setDefaultJsonOption->type_name("PATH"); setDefaultJsonOption->type_size(1, 1); setDefaultJsonOption->expected(0, 1); app.add_flag("--dump-json", dumpRuntimeJson, "Print the runtime JSON that was loaded"); app.add_flag("--trace", traceRuntime, "Emit a log line when key functions/methods run"); try { app.parse(argc, argv); } catch (const CLI::CallForHelp& e) { std::exit(app.exit(e)); } catch (const CLI::CallForVersion& e) { std::exit(app.exit(e)); } catch (const CLI::ParseError& e) { app.exit(e); throw; } bool shouldSaveDefault = setDefaultJsonOption->count() > 0; std::optional providedDefaultPath; if (shouldSaveDefault && !setDefaultJsonPath.empty()) { providedDefaultPath = std::filesystem::absolute(setDefaultJsonPath); } RuntimeConfig runtimeConfig; if (!jsonInputText.empty()) { runtimeConfig = LoadRuntimeConfigFromJson(std::filesystem::absolute(jsonInputText), dumpRuntimeJson); } else if (providedDefaultPath) { runtimeConfig = LoadRuntimeConfigFromJson(*providedDefaultPath, dumpRuntimeJson); } else if (auto defaultPath = GetDefaultConfigPath(); defaultPath && std::filesystem::exists(*defaultPath)) { runtimeConfig = LoadRuntimeConfigFromJson(*defaultPath, dumpRuntimeJson); } else { runtimeConfig = GenerateDefaultRuntimeConfig(argc > 0 ? argv[0] : nullptr); } AppOptions options; options.runtimeConfig = std::move(runtimeConfig); if (!seedOutputText.empty()) { options.seedOutput = std::filesystem::absolute(seedOutputText); } options.saveDefaultJson = shouldSaveDefault; options.dumpRuntimeJson = dumpRuntimeJson; options.traceEnabled = traceRuntime; return options; } void LogRuntimeConfig(const RuntimeConfig& config) { TRACE_VAR(config.width); TRACE_VAR(config.height); TRACE_VAR(config.scriptPath); } void WriteRuntimeConfigJson(const RuntimeConfig& runtimeConfig, const std::filesystem::path& configPath) { rapidjson::Document document; document.SetObject(); auto& allocator = document.GetAllocator(); auto addStringMember = [&](const char* name, const std::string& value) { rapidjson::Value nameValue(name, allocator); rapidjson::Value stringValue(value.c_str(), allocator); document.AddMember(nameValue, stringValue, allocator); }; document.AddMember("window_width", runtimeConfig.width, allocator); document.AddMember("window_height", runtimeConfig.height, allocator); addStringMember("lua_script", runtimeConfig.scriptPath.string()); std::filesystem::path scriptsDir = runtimeConfig.scriptPath.parent_path(); addStringMember("scripts_directory", scriptsDir.string()); std::filesystem::path projectRoot = scriptsDir.parent_path(); if (!projectRoot.empty()) { addStringMember("project_root", projectRoot.string()); addStringMember("shaders_directory", (projectRoot / "shaders").string()); } else { addStringMember("shaders_directory", "shaders"); } rapidjson::Value extensionArray(rapidjson::kArrayType); for (const char* extension : sdl3cpp::app::kDeviceExtensions) { rapidjson::Value extensionValue(extension, allocator); extensionArray.PushBack(extensionValue, allocator); } document.AddMember("device_extensions", extensionArray, allocator); addStringMember("config_file", configPath.string()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); document.Accept(writer); auto parentDir = configPath.parent_path(); if (!parentDir.empty()) { std::filesystem::create_directories(parentDir); } std::ofstream outFile(configPath); if (!outFile) { throw std::runtime_error("Failed to open config output file: " + configPath.string()); } outFile << buffer.GetString(); } } // namespace int main(int argc, char** argv) { SDL_SetMainReady(); SetupSignalHandlers(); try { AppOptions options = ParseCommandLine(argc, argv); sdl3cpp::app::TraceLogger::SetEnabled(options.traceEnabled); // Initialize logger auto& logger = sdl3cpp::logging::Logger::GetInstance(); if (options.traceEnabled) { logger.SetLevel(sdl3cpp::logging::LogLevel::TRACE); } else { logger.SetLevel(sdl3cpp::logging::LogLevel::INFO); } logger.EnableConsoleOutput(true); LOG_INFO("Application starting"); LogRuntimeConfig(options.runtimeConfig); if (options.seedOutput) { WriteRuntimeConfigJson(options.runtimeConfig, *options.seedOutput); } if (options.saveDefaultJson) { if (auto defaultPath = GetDefaultConfigPath()) { WriteRuntimeConfigJson(options.runtimeConfig, *defaultPath); } else { throw std::runtime_error("Unable to determine platform config directory"); } } sdl3cpp::app::Sdl3App app(options.runtimeConfig.scriptPath, options.runtimeConfig.luaDebug); app.Run(); } catch (const std::runtime_error& e) { std::string errorMsg = e.what(); LOG_ERROR("Runtime error: " + errorMsg); // Check if this is a timeout/hang error - show simpler message for these bool isTimeoutError = errorMsg.find("timeout") != std::string::npos || errorMsg.find("Launch timeout") != std::string::npos || errorMsg.find("Swapchain recreation loop") != std::string::npos; if (!isTimeoutError) { // For non-timeout errors, show full error dialog SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Application Error", errorMsg.c_str(), nullptr); } else { // For timeout errors, the console output already has diagnostic info // Just show a brief dialog std::string briefMsg = "Application failed to launch. Check console output for details."; SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Launch Failed", briefMsg.c_str(), nullptr); } return EXIT_FAILURE; } catch (const std::exception& e) { LOG_ERROR("Exception: " + std::string(e.what())); SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Application Error", e.what(), nullptr); return EXIT_FAILURE; } return EXIT_SUCCESS; }