#include "window.hpp" #include "init/splash_window.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(OS_WEB) #define GLFW_INCLUDE_ES3 #include #include #else #include #endif using namespace std::literals::chrono_literals; namespace hex::init { struct GlfwError { int errorCode = 0; std::string desc; }; GlfwError lastGlfwError; WindowSplash::WindowSplash() : m_window(nullptr) { this->initGLFW(); this->initImGui(); this->initMyself(); ImHexApi::System::impl::setGPUVendor(reinterpret_cast(glGetString(GL_VENDOR))); } WindowSplash::~WindowSplash() { this->exitImGui(); this->exitGLFW(); } static void centerWindow(GLFWwindow *window) { // Get the primary monitor GLFWmonitor *monitor = glfwGetPrimaryMonitor(); if (!monitor) return; // Get information about the monitor const GLFWvidmode *mode = glfwGetVideoMode(monitor); if (!mode) return; // Get the position of the monitor's viewport on the virtual screen int monitorX, monitorY; glfwGetMonitorPos(monitor, &monitorX, &monitorY); // Get the window size int windowWidth, windowHeight; glfwGetWindowSize(window, &windowWidth, &windowHeight); // Center the splash screen on the monitor glfwSetWindowPos(window, monitorX + (mode->width - windowWidth) / 2, monitorY + (mode->height - windowHeight) / 2); } std::future WindowSplash::processTasksAsync() { return std::async(std::launch::async, [this] { bool status = true; std::atomic tasksCompleted = 0; // Loop over all registered init tasks for (const auto &[name, task, async] : this->m_tasks) { // Construct a new task callback auto runTask = [&, task, name] { try { // Save an iterator to the current task name decltype(this->m_currTaskNames)::iterator taskNameIter; { std::lock_guard guard(this->m_progressMutex); this->m_currTaskNames.push_back(name + "..."); taskNameIter = std::prev(this->m_currTaskNames.end()); } // When the task finished, increment the progress bar ON_SCOPE_EXIT { tasksCompleted += 1; this->m_progress = float(tasksCompleted) / this->m_tasks.size(); }; // Execute the actual task and track the amount of time it took to run auto startTime = std::chrono::high_resolution_clock::now(); bool taskStatus = task(); auto endTime = std::chrono::high_resolution_clock::now(); log::info("Task '{}' finished {} in {} ms", name, taskStatus ? "successfully" : "unsuccessfully", std::chrono::duration_cast(endTime - startTime).count() ); // Track the overall status of the tasks status = status && taskStatus; // Erase the task name from the list of running tasks { std::lock_guard guard(this->m_progressMutex); this->m_currTaskNames.erase(taskNameIter); } } catch (const std::exception &e) { log::error("Init task '{}' threw an exception: {}", name, e.what()); status = false; } catch (...) { log::error("Init task '{}' threw an unidentifiable exception", name); status = false; } }; // If the task can be run asynchronously, run it in a separate thread // otherwise run it in this thread and wait for it to finish if (async) { std::thread([runTask = std::move(runTask)]{ runTask(); }).detach(); } else { runTask(); } } // Check every 100ms if all tasks have run while (tasksCompleted < this->m_tasks.size()) { std::this_thread::sleep_for(100ms); } // Small extra delay so the last progress step is visible std::this_thread::sleep_for(100ms); return status; }); } FrameResult WindowSplash::fullFrame() { glfwSetWindowSize(this->m_window, 640_scaled, 400_scaled); centerWindow(this->m_window); glfwPollEvents(); // Start a new ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); auto scale = ImHexApi::System::getGlobalScale(); // Draw the splash screen background auto drawList = ImGui::GetBackgroundDrawList(); { // Draw the splash screen background drawList->AddImage(this->splashBackgroundTexture, ImVec2(0, 0), this->splashBackgroundTexture.getSize() * scale); { // Function to highlight a given number of bytes at a position in the splash screen const auto highlightBytes = [&](ImVec2 start, size_t count, ImColor color, float opacity) { // Dimensions and number of bytes that are drawn. Taken from the splash screen image const auto hexSize = ImVec2(29, 18) * scale; const auto hexSpacing = ImVec2(17.4, 15) * scale; const auto hexStart = ImVec2(27, 127) * scale; constexpr auto HexCount = ImVec2(13, 7); bool isStart = true; color.Value.w *= opacity; // Loop over all the bytes on the splash screen for (u32 y = u32(start.y); y < u32(HexCount.y); y += 1) { for (u32 x = u32(start.x); x < u32(HexCount.x); x += 1) { if (count-- == 0) return; // Find the start position of the byte to draw auto pos = hexStart + ImVec2(float(x), float(y)) * (hexSize + hexSpacing); // Fill the rectangle in the byte with the given color drawList->AddRectFilled(pos + ImVec2(0, -hexSpacing.y / 2), pos + hexSize + ImVec2(0, hexSpacing.y / 2), color); // Add some extra color on the right if the current byte isn't the last byte, and we didn't reach the right side of the image if (count > 0 && x != u32(HexCount.x) - 1) drawList->AddRectFilled(pos + ImVec2(hexSize.x, -hexSpacing.y / 2), pos + hexSize + ImVec2(hexSpacing.x, hexSpacing.y / 2), color); // Add some extra color on the left if this is the first byte we're highlighting if (isStart) { isStart = false; drawList->AddRectFilled(pos - hexSpacing / 2, pos + ImVec2(0, hexSize.y + hexSpacing.y / 2), color); } // Add some extra color on the right if this is the last byte if (count == 0) { drawList->AddRectFilled(pos + ImVec2(hexSize.x, -hexSpacing.y / 2), pos + hexSize + hexSpacing / 2, color); } } start.x = 0; } }; // Draw all highlights, slowly fading them in as the init tasks progress for (const auto &highlight : this->highlights) highlightBytes(highlight.start, highlight.count, highlight.color, this->progressLerp); } this->progressLerp += (this->m_progress - this->progressLerp) * 0.1F; // Draw the splash screen foreground drawList->AddImage(this->splashTextTexture, ImVec2(0, 0), this->splashTextTexture.getSize() * scale); // Draw the "copyright" notice drawList->AddText(ImVec2(35, 85) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("WerWolv\n2020 - {0}", &__DATE__[7]).c_str()); // Draw version information // In debug builds, also display the current commit hash and branch #if defined(DEBUG) const static auto VersionInfo = hex::format("{0} : {1} {2}@{3}", ImHexApi::System::getImHexVersion(), ICON_FA_CODE_BRANCH, ImHexApi::System::getCommitBranch(), ImHexApi::System::getCommitHash()); #else const static auto VersionInfo = hex::format("{0}", ImHexApi::System::getImHexVersion()); #endif drawList->AddText(ImVec2((this->splashBackgroundTexture.getSize().x * scale - ImGui::CalcTextSize(VersionInfo.c_str()).x) / 2, 105 * scale), ImColor(0xFF, 0xFF, 0xFF, 0xFF), VersionInfo.c_str()); } // Draw the task progress bar { std::lock_guard guard(this->m_progressMutex); const auto progressBackgroundStart = ImVec2(99, 357) * scale; const auto progressBackgroundSize = ImVec2(442, 30) * scale; const auto progressStart = progressBackgroundStart + ImVec2(0, 20) * scale; const auto progressSize = ImVec2(progressBackgroundSize.x * this->m_progress, 10 * scale); // Draw progress bar drawList->AddRectFilled(progressStart, progressStart + progressSize, 0xD0FFFFFF); // Draw task names separated by | characters if (!this->m_currTaskNames.empty()) { drawList->PushClipRect(progressBackgroundStart, progressBackgroundStart + progressBackgroundSize, true); drawList->AddText(progressStart + ImVec2(5, -20) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("{}", fmt::join(this->m_currTaskNames, " | ")).c_str()); drawList->PopClipRect(); } } // Render the frame ImGui::Render(); int displayWidth, displayHeight; glfwGetFramebufferSize(this->m_window, &displayWidth, &displayHeight); glViewport(0, 0, displayWidth, displayHeight); glClearColor(0.00F, 0.00F, 0.00F, 0.00F); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(this->m_window); // Check if all background tasks have finished so the splash screen can be closed if (this->tasksSucceeded.wait_for(0s) == std::future_status::ready) { if (this->tasksSucceeded.get()) { log::debug("All tasks finished with success !"); return FrameResult::success; } else { log::warn("All tasks finished, but some failed"); return FrameResult::failure; } } return FrameResult::wait; } bool WindowSplash::loop() { // Splash window rendering loop while (true) { auto res = this->fullFrame(); if (res == FrameResult::success) return true; else if (res == FrameResult::failure) return false; } } void WindowSplash::initGLFW() { glfwSetErrorCallback([](int errorCode, const char *desc) { lastGlfwError.errorCode = errorCode; lastGlfwError.desc = std::string(desc); log::error("GLFW Error [{}] : {}", errorCode, desc); }); if (!glfwInit()) { log::fatal("Failed to initialize GLFW!"); std::exit(EXIT_FAILURE); } // Configure used OpenGL version #if defined(OS_MACOS) glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE); glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, GLFW_TRUE); #else glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); #endif // Make splash screen non-resizable, undecorated and transparent glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); glfwWindowHint(GLFW_FLOATING, GLFW_FALSE); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); // Create the splash screen window this->m_window = glfwCreateWindow(1, 1, "Starting ImHex...", nullptr, nullptr); if (this->m_window == nullptr) { hex::nativeErrorMessage(hex::format( "Failed to create GLFW window: [{}] {}.\n" "You may not have a renderer available.\n" "The most common cause of this is using a virtual machine\n" "You may want to try a release artifact ending with 'NoGPU'" , lastGlfwError.errorCode, lastGlfwError.desc)); std::exit(EXIT_FAILURE); } // Force window to be fully opaque by default glfwSetWindowOpacity(this->m_window, 1.0F); // Calculate native scale factor for hidpi displays { float xScale = 0, yScale = 0; glfwGetWindowContentScale(this->m_window, &xScale, &yScale); auto meanScale = std::midpoint(xScale, yScale); if (meanScale <= 0.0F) meanScale = 1.0F; #if defined(OS_MACOS) meanScale /= getBackingScaleFactor(); #elif defined(OS_WEB) meanScale = 1.0F; #endif ImHexApi::System::impl::setGlobalScale(meanScale); ImHexApi::System::impl::setNativeScale(meanScale); log::info("Native scaling set to: {:.1f}", meanScale); } glfwMakeContextCurrent(this->m_window); glfwSwapInterval(1); } void WindowSplash::initImGui() { // Initialize ImGui IMGUI_CHECKVERSION(); GImGui = ImGui::CreateContext(); ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL(this->m_window, true); #if defined(OS_MACOS) ImGui_ImplOpenGL3_Init("#version 150"); #elif defined(OS_WEB) ImGui_ImplOpenGL3_Init(); #else ImGui_ImplOpenGL3_Init("#version 130"); #endif auto &io = ImGui::GetIO(); ImGui::GetStyle().ScaleAllSizes(ImHexApi::System::getGlobalScale()); // Load fonts necessary for the splash screen { io.Fonts->Clear(); ImFontConfig cfg; cfg.OversampleH = cfg.OversampleV = 1, cfg.PixelSnapH = true; cfg.SizePixels = 13.0_scaled; io.Fonts->AddFontDefault(&cfg); cfg.MergeMode = true; ImWchar fontAwesomeRange[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; std::uint8_t *px; int w, h; io.Fonts->AddFontFromMemoryCompressedTTF(font_awesome_compressed_data, font_awesome_compressed_size, 11.0_scaled, &cfg, fontAwesomeRange); io.Fonts->GetTexDataAsAlpha8(&px, &w, &h); // Create new font atlas GLuint tex; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_ALPHA, GL_UNSIGNED_BYTE, px); io.Fonts->SetTexID(reinterpret_cast(tex)); } // Don't save window settings for the splash screen io.IniFilename = nullptr; } /** * @brief Initialize resources for the splash window */ void WindowSplash::initMyself() { // Load splash screen image from romfs this->splashBackgroundTexture = ImGuiExt::Texture(romfs::get("splash_background.png").span()); this->splashTextTexture = ImGuiExt::Texture(romfs::get("splash_text.png").span()); // If the image couldn't be loaded correctly, something went wrong during the build process // Close the application since this would lead to errors later on anyway. if (!this->splashBackgroundTexture.isValid() || !this->splashTextTexture.isValid()) { log::fatal("Could not load splash screen image!"); std::exit(EXIT_FAILURE); } std::mt19937 rng(std::random_device{}()); u32 lastPos = 0; u32 lastCount = 0; for (auto &highlight : this->highlights) { auto newPos = lastPos + lastCount + (rng() % 40); auto newCount = (rng() % 7) + 3; highlight.start.x = newPos % 13; highlight.start.y = newPos / 13; highlight.count = newCount; { float r, g, b; ImGui::ColorConvertHSVtoRGB( (rng() % 360) / 100.0F, (25 + rng() % 70) / 100.0F, (85 + rng() % 10) / 100.0F, r, g, b); highlight.color = ImColor(r, g, b, 0x50 / 255.0F); } lastPos = newPos; lastCount = newCount; } } void WindowSplash::startStartupTasks() { // Launch init tasks in the background this->tasksSucceeded = processTasksAsync(); } void WindowSplash::exitGLFW() { glfwDestroyWindow(this->m_window); glfwTerminate(); } void WindowSplash::exitImGui() { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); } }