// src/main.cpp #include #include #include #include #include #include #include #include #include #include "audio/jail_audio.hpp" #include "defines.hpp" #include "rendering/shader_backend.hpp" struct Logger { static void info(const std::string& s) { std::cout << "[INFO] " << s << '\n'; } static void error(const std::string& s) { std::cerr << "[ERROR] " << s << '\n'; } }; struct VideoOptions { bool fullscreen = false; bool vsync = true; } Options_video; struct DisplayMonitor { std::string name; int width = 0; int height = 0; int refresh_rate = 0; }; static DisplayMonitor display_monitor_; static SDL_Window* window_ = nullptr; static std::unique_ptr backend_; struct ShaderEntry { std::filesystem::path folder; std::string base_name; }; static std::vector shader_list_; static std::vector shader_names_; static std::vector shader_authors_; static size_t current_shader_index_ = 0; static std::filesystem::path shaders_directory_; static Uint32 shader_start_ticks_ = 0; static Uint32 fps_frame_count_ = 0; static Uint32 fps_last_update_ticks_ = 0; static float current_fps_ = 0.0f; static std::unique_ptr audio_engine_; static std::vector music_list_; static std::vector music_names_; static size_t current_music_index_ = 0; static bool music_muted_ = false; static std::vector scanShaderDirectory(const std::filesystem::path& directory) { std::vector shaders; if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) { Logger::error("Shader directory does not exist: " + directory.string()); return shaders; } for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (!entry.is_directory()) { continue; } const std::string folder_name = entry.path().filename().string(); if (folder_name.empty() || folder_name[0] == '_' || folder_name[0] == '.') { continue; } const std::filesystem::path gl_source = entry.path() / (folder_name + ".gl.glsl"); if (!std::filesystem::exists(gl_source)) { Logger::info("Skipping " + folder_name + ": missing " + gl_source.filename().string()); continue; } shaders.push_back(ShaderEntry{entry.path(), folder_name}); } std::sort(shaders.begin(), shaders.end(), [](const ShaderEntry& a, const ShaderEntry& b) { return a.base_name < b.base_name; }); Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string()); shader_names_.resize(shaders.size(), ""); shader_authors_.resize(shaders.size(), ""); return shaders; } static void preloadMusicDirectory(const std::filesystem::path& directory) { if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) { Logger::info("Music directory does not exist: " + directory.string()); return; } std::vector ogg_paths; for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (entry.is_regular_file() && entry.path().extension() == ".ogg") { ogg_paths.push_back(entry.path()); } } std::sort(ogg_paths.begin(), ogg_paths.end()); for (const auto& path : ogg_paths) { std::size_t size = 0; void* raw = SDL_LoadFile(path.string().c_str(), &size); if (raw == nullptr || size == 0) { Logger::error("Failed to read music file: " + path.string()); if (raw != nullptr) { SDL_free(raw); } continue; } Ja::Music* music = Ja::loadMusic(static_cast(raw), static_cast(size), path.filename().string().c_str()); SDL_free(raw); if (music == nullptr) { Logger::error("Failed to decode OGG: " + path.string()); continue; } music_list_.push_back(music); music_names_.push_back(path.filename().string()); } Logger::info("Preloaded " + std::to_string(music_list_.size()) + " music file(s) from " + directory.string()); } static void playRandomMusic() { if (music_list_.empty() || !audio_engine_) { return; } current_music_index_ = static_cast(rand()) % music_list_.size(); audio_engine_->playMusic(music_list_[current_music_index_], 0); Logger::info("Now playing: " + music_names_[current_music_index_]); } static void updateWindowTitle() { if (window_ == nullptr || shader_list_.empty()) { return; } std::string shaderName; if (!shader_names_.empty() && !shader_names_[current_shader_index_].empty()) { shaderName = shader_names_[current_shader_index_]; } else { shaderName = shader_list_[current_shader_index_].base_name; } if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) { shaderName += " by " + shader_authors_[current_shader_index_]; } std::string title = WINDOW_TITLE_PREFIX; title += " ("; title += shaderName; if (backend_) { title += " - "; title += backend_->driverName(); } if (current_fps_ > 0.0f) { title += " - "; title += std::to_string(static_cast(current_fps_ + 0.5f)) + " FPS"; } if (Options_video.vsync) { title += " - VSync"; } title += ")"; SDL_SetWindowTitle(window_, title.c_str()); } static bool loadShaderAtIndex(size_t index) { if (index >= shader_list_.size()) { Logger::error("Invalid shader index: " + std::to_string(index)); return false; } const auto& entry = shader_list_[index]; Logger::info("Loading shader: " + entry.folder.string()); Rendering::ShaderProgramSpec spec; spec.folder = entry.folder; spec.base_name = entry.base_name; const std::filesystem::path meta_path = entry.folder / "meta.txt"; if (std::filesystem::exists(meta_path)) { spec.metadata = Rendering::parseMetaFile(meta_path); } else { std::string source; const std::filesystem::path gl_source = entry.folder / (entry.base_name + ".gl.glsl"); if (Rendering::loadFileToString(gl_source, source)) { spec.metadata = Rendering::extractShaderMetadata(source); } } if (!spec.metadata.name.empty()) { shader_names_[index] = spec.metadata.name; Logger::info("Shader name: " + spec.metadata.name); } if (!spec.metadata.author.empty()) { shader_authors_[index] = spec.metadata.author; Logger::info("Shader author: " + spec.metadata.author); } return backend_->loadShader(spec); } void getDisplayInfo() { int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays != nullptr && num_displays > 0) { for (int i = 0; i < num_displays; ++i) { const SDL_DisplayID instance_id = displays[i]; const char* name = SDL_GetDisplayName(instance_id); Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown")); } const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(displays[0]); const char* first_display_name = SDL_GetDisplayName(displays[0]); display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown"; if (dm != nullptr) { display_monitor_.width = static_cast(dm->w); display_monitor_.height = static_cast(dm->h); display_monitor_.refresh_rate = static_cast(dm->refresh_rate); } else { Logger::info("SDL_GetCurrentDisplayMode returned null"); } } else { Logger::info("No displays found or SDL_GetDisplays failed"); } } void setFullscreenMode() { if (window_ == nullptr) { return; } if (Options_video.fullscreen) { if (!SDL_SetWindowFullscreen(window_, true)) { Logger::error(std::string("Failed to set fullscreen: ") + SDL_GetError()); Logger::info("Fallback to windowed mode 800x800"); SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT); Options_video.fullscreen = false; SDL_ShowCursor(); } else { SDL_HideCursor(); } } else { SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT); SDL_ShowCursor(); } } void toggleFullscreen() { Options_video.fullscreen = !Options_video.fullscreen; setFullscreenMode(); } void toggleVSync() { Options_video.vsync = !Options_video.vsync; if (backend_) { backend_->setVSync(Options_video.vsync); } } void switchShader(int direction) { if (shader_list_.empty()) { return; } size_t new_index = current_shader_index_; if (direction > 0) { new_index = (current_shader_index_ + 1) % shader_list_.size(); } else if (direction < 0) { new_index = (current_shader_index_ == 0) ? shader_list_.size() - 1 : current_shader_index_ - 1; } if (!loadShaderAtIndex(new_index)) { Logger::error("Failed to switch shader, keeping current one"); return; } current_shader_index_ = new_index; shader_start_ticks_ = SDL_GetTicks(); updateWindowTitle(); } void handleDebugEvents(const SDL_Event& event) { if (event.type == SDL_EVENT_KEY_DOWN && static_cast(event.key.repeat) == 0) { switch (event.key.key) { case SDLK_F3: { toggleFullscreen(); break; } case SDLK_F4: { toggleVSync(); break; } case SDLK_M: { music_muted_ = !music_muted_; if (audio_engine_) { audio_engine_->setMusicVolume(music_muted_ ? 0.0f : 1.0f); } Logger::info(music_muted_ ? "Music muted" : "Music unmuted"); break; } case SDLK_LEFT: { switchShader(-1); break; } case SDLK_RIGHT: { switchShader(+1); break; } default: break; } } } enum class BackendChoice { Auto, Gpu, OpenGL }; static auto createWindowForBackend(BackendChoice choice) -> SDL_Window* { SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; if (choice == BackendChoice::OpenGL) { flags |= SDL_WINDOW_OPENGL; SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); } return SDL_CreateWindow(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT, flags); } int main(int argc, char** argv) { std::string shaderPath; bool fullscreenFlag = false; BackendChoice backend_choice = BackendChoice::Auto; for (int i = 1; i < argc; ++i) { const std::string a = argv[i]; if (a == "-F" || a == "--fullscreen") { fullscreenFlag = true; continue; } if (a == "--backend=gpu") { backend_choice = BackendChoice::Gpu; continue; } if (a == "--backend=opengl") { backend_choice = BackendChoice::OpenGL; continue; } if (a == "--backend=auto") { backend_choice = BackendChoice::Auto; continue; } if (shaderPath.empty()) { shaderPath = a; } } if (shaderPath.empty()) { shaderPath = "test"; } Options_video.fullscreen = fullscreenFlag; auto initResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); if constexpr (std::is_same_v) { if (!initResult) { Logger::error(SDL_GetError()); return -1; } } else { if (initResult != 0) { Logger::error(SDL_GetError()); return -1; } } getDisplayInfo(); if (backend_choice != BackendChoice::OpenGL) { window_ = createWindowForBackend(BackendChoice::Gpu); if (window_ != nullptr) { backend_ = Rendering::makeSdl3GpuBackend(); if (!backend_->init(window_)) { Logger::info("SDL3 GPU backend init failed, falling back to OpenGL"); backend_.reset(); SDL_DestroyWindow(window_); window_ = nullptr; if (backend_choice == BackendChoice::Gpu) { SDL_Quit(); return -1; } } } } if (backend_ == nullptr) { window_ = createWindowForBackend(BackendChoice::OpenGL); if (window_ == nullptr) { Logger::error(std::string("SDL_CreateWindow error: ") + SDL_GetError()); SDL_Quit(); return -1; } backend_ = Rendering::makeOpenGLBackend(); if (!backend_->init(window_)) { Logger::error("Failed to initialize shader backend"); SDL_DestroyWindow(window_); SDL_Quit(); return -1; } } setFullscreenMode(); backend_->setVSync(Options_video.vsync); audio_engine_ = std::make_unique(48000, SDL_AUDIO_S16, 2); audio_engine_->setOnMusicEnded([]() { playRandomMusic(); }); const std::string resources_dir = getResourcesDirectory(); srand(static_cast(time(nullptr))); const std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music"; preloadMusicDirectory(music_directory); if (!music_list_.empty()) { playRandomMusic(); } else { Logger::info("No music files found in " + music_directory.string()); } const std::filesystem::path arg_path(shaderPath); std::filesystem::path target_folder; if (arg_path.has_parent_path()) { target_folder = arg_path; shaders_directory_ = arg_path.parent_path(); } else { shaders_directory_ = std::filesystem::path(resources_dir) / "data" / "shaders"; target_folder = shaders_directory_ / shaderPath; } shader_list_ = scanShaderDirectory(shaders_directory_); if (shader_list_.empty()) { Logger::error("No shaders found in directory: " + shaders_directory_.string()); backend_->cleanup(); SDL_DestroyWindow(window_); SDL_Quit(); return -1; } size_t initial_index = 0; bool found_shader = false; for (size_t i = 0; i < shader_list_.size(); ++i) { if (shader_list_[i].folder == target_folder) { initial_index = i; found_shader = true; break; } } if (!found_shader) { const std::filesystem::path default_folder = std::filesystem::path(resources_dir) / "data" / "shaders" / "test"; for (size_t i = 0; i < shader_list_.size(); ++i) { if (shader_list_[i].folder == default_folder) { initial_index = i; found_shader = true; break; } } } if (!found_shader) { Logger::info("Specified shader not found, using first shader in directory"); initial_index = 0; } current_shader_index_ = initial_index; if (!loadShaderAtIndex(current_shader_index_)) { Logger::error("Failed to load initial shader"); backend_->cleanup(); SDL_DestroyWindow(window_); SDL_Quit(); return -1; } shader_start_ticks_ = SDL_GetTicks(); fps_last_update_ticks_ = SDL_GetTicks(); updateWindowTitle(); bool running = true; while (running) { fps_frame_count_++; const Uint32 current_ticks = SDL_GetTicks(); if (current_ticks - fps_last_update_ticks_ >= 500) { const float elapsed_seconds = static_cast(current_ticks - fps_last_update_ticks_) / 1000.0f; current_fps_ = static_cast(fps_frame_count_) / elapsed_seconds; fps_frame_count_ = 0; fps_last_update_ticks_ = current_ticks; updateWindowTitle(); } if (audio_engine_) { audio_engine_->update(); } SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_EVENT_QUIT) { running = false; } else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { running = false; } else if (e.type == SDL_EVENT_KEY_DOWN) { if (e.key.key == SDLK_ESCAPE) { running = false; } handleDebugEvents(e); } } Rendering::ShaderUniforms uniforms; uniforms.iTime = static_cast(SDL_GetTicks() - shader_start_ticks_) / 1000.0f; int w = 0; int h = 0; SDL_GetWindowSize(window_, &w, &h); uniforms.iResolutionX = static_cast(w); uniforms.iResolutionY = static_cast(h); backend_->render(uniforms); if (!Options_video.vsync) { SDL_Delay(1); } } backend_->cleanup(); backend_.reset(); for (Ja::Music* m : music_list_) { Ja::deleteMusic(m); } music_list_.clear(); music_names_.clear(); audio_engine_.reset(); SDL_DestroyWindow(window_); SDL_Quit(); return 0; }