| // Copyright 2022 The Pigweed Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
| // use this file except in compliance with the License. You may obtain a copy of | |
| // the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
| // License for the specific language governing permissions and limitations under | |
| // the License. | |
| // | |
| // LCD Facade using imgui running on a host machine. | |
| // Much of this code is from the imgui example: | |
| // https://github.com/ocornut/imgui/tree/master/examples/example_glfw_opengl3 | |
| // As well as the wiki page: | |
| // https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples | |
| #include "pw_display_driver_imgui/display_driver.h" | |
| // To silence large number of warnings (at least on macOS). | |
| #define GL_SILENCE_DEPRECATION | |
| #if defined(IMGUI_IMPL_OPENGL_ES2) | |
| #include <GLES2/gl2.h> | |
| #endif | |
| #include <GLFW/glfw3.h> // Will pull in system OpenGL headers | |
| #include "imgui.h" | |
| #include "imgui_impl_glfw.h" | |
| #include "imgui_impl_opengl3.h" | |
| #include "pw_framebuffer/reader.h" | |
| using pw::color::color_rgb565_t; | |
| using pw::framebuffer::Framebuffer; | |
| using pw::framebuffer::FramebufferReader; | |
| using pw::framebuffer::PixelFormat; | |
| namespace pw::display_driver { | |
| namespace { | |
| constexpr uint16_t kDisplayWidth = 320; | |
| constexpr uint16_t kDisplayHeight = 240; | |
| constexpr size_t kDisplayDataSize = kDisplayWidth * kDisplayHeight; | |
| // OpenGL texture data. | |
| GLuint lcd_pixel_data[kDisplayDataSize]; | |
| // imgui state | |
| bool show_imgui_demo_window = false; | |
| ImVec4 clear_color = ImVec4(0.27f, 0.27f, 0.27f, 1.00f); | |
| GLuint lcd_texture = 0; | |
| GLFWwindow* window; | |
| int lcd_texture_display_scale = 3; | |
| int old_lcd_texture_display_scale = 0; | |
| bool lcd_texture_display_mode_nearest = true; | |
| bool old_lcd_texture_display_mode_nearest = true; | |
| ImGuiMousePosition touchscreen_mouse_position; | |
| void CleanupAndExit() { | |
| ImGui_ImplOpenGL3_Shutdown(); | |
| ImGui_ImplGlfw_Shutdown(); | |
| ImGui::DestroyContext(); | |
| glfwDestroyWindow(window); | |
| glfwTerminate(); | |
| exit(0); | |
| } | |
| void HelpMarker(const char* desc) { | |
| ImGui::TextDisabled("(?)"); | |
| if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { | |
| ImGui::BeginTooltip(); | |
| ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); | |
| ImGui::TextUnformatted(desc); | |
| ImGui::PopTextWrapPos(); | |
| ImGui::EndTooltip(); | |
| } | |
| } | |
| void _SetTexturePixel( | |
| GLuint x, GLuint y, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { | |
| // Calculate target color | |
| GLuint target_color; | |
| GLubyte* colors = (GLubyte*)&target_color; | |
| colors[0] = r; | |
| colors[1] = g; | |
| colors[2] = b; | |
| colors[3] = a; | |
| lcd_pixel_data[y * kDisplayWidth + x] = target_color; | |
| } | |
| void _SetTexturePixel(GLuint x, GLuint y, uint8_t r, uint8_t g, uint8_t b) { | |
| _SetTexturePixel(x, y, r, g, b, 255); | |
| } | |
| void _SetTexturePixel(GLuint x, GLuint y, color_rgb565_t rgb565) { | |
| pw::color::ColorRGBA c(rgb565); | |
| _SetTexturePixel(x, y, c.r, c.g, c.b, 255); | |
| } | |
| void UpdateLcdTexture() { | |
| // Set current texture | |
| glBindTexture(GL_TEXTURE_2D, lcd_texture); | |
| // Update texture | |
| glTexSubImage2D(GL_TEXTURE_2D, | |
| 0, | |
| 0, | |
| 0, | |
| kDisplayWidth, | |
| kDisplayHeight, | |
| GL_RGBA, | |
| GL_UNSIGNED_BYTE, | |
| lcd_pixel_data); | |
| // Unbind texture | |
| glBindTexture(GL_TEXTURE_2D, 0); | |
| } | |
| void SetupLcdTexture(GLuint* out_texture) { | |
| // Create a OpenGL texture identifier | |
| GLuint image_texture; | |
| glGenTextures(1, &image_texture); | |
| glBindTexture(GL_TEXTURE_2D, image_texture); | |
| // Setup filtering parameters for display | |
| GLuint display_mode = | |
| lcd_texture_display_mode_nearest ? GL_NEAREST : GL_LINEAR; | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, display_mode); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, display_mode); | |
| glTexParameteri(GL_TEXTURE_2D, | |
| GL_TEXTURE_WRAP_S, | |
| GL_CLAMP_TO_EDGE); // This is required on WebGL for non | |
| // power-of-two textures | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Same | |
| // Upload pixels into texture | |
| #if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__) | |
| glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); | |
| #endif | |
| glTexImage2D(GL_TEXTURE_2D, | |
| 0, | |
| GL_RGBA, | |
| kDisplayWidth, | |
| kDisplayHeight, | |
| 0, | |
| GL_RGBA, | |
| GL_UNSIGNED_BYTE, | |
| lcd_pixel_data); | |
| glBindTexture(GL_TEXTURE_2D, 0); | |
| *out_texture = image_texture; | |
| } | |
| void glfw_error_callback(int error, const char* description) { | |
| fprintf(stderr, "Glfw Error %d: %s\n", error, description); | |
| } | |
| } // namespace | |
| DisplayDriverImgUI::DisplayDriverImgUI() = default; | |
| Status DisplayDriverImgUI::Init() { | |
| // Setup window | |
| glfwSetErrorCallback(glfw_error_callback); | |
| if (!glfwInit()) | |
| return Status::Internal(); | |
| // Decide GL+GLSL versions | |
| #if defined(IMGUI_IMPL_OPENGL_ES2) | |
| // GL ES 2.0 + GLSL 100 | |
| const char* glsl_version = "#version 100"; | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); | |
| #elif defined(__APPLE__) | |
| // GL 3.2 + GLSL 150 | |
| const char* glsl_version = "#version 150"; | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | |
| glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only | |
| glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac | |
| #else | |
| // GL 3.0 + GLSL 130 | |
| const char* glsl_version = "#version 130"; | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | |
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); | |
| // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ | |
| // only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only | |
| #endif | |
| // Create window with graphics context | |
| window = glfwCreateWindow(1280, 800, "pw_display", NULL, NULL); | |
| if (window == NULL) | |
| return Status::Internal(); | |
| glfwMakeContextCurrent(window); | |
| glfwSwapInterval(1); // Enable vsync | |
| // Setup Dear ImGui context | |
| IMGUI_CHECKVERSION(); | |
| ImGui::CreateContext(); | |
| ImGuiIO& io = ImGui::GetIO(); | |
| (void)io; | |
| // io.Fonts->AddFontFromFileTTF("NotoSans-Regular.ttf", 32.0); | |
| // Enable Keyboard Controls | |
| io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; | |
| io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; | |
| io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleFonts; | |
| // Enable Gamepad Controls | |
| // io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; | |
| // Setup Dear ImGui style | |
| ImGui::StyleColorsDark(); | |
| // ImGui::StyleColorsLight(); | |
| // ImGui::StyleColorsClassic(); | |
| // Setup Platform/Renderer backends | |
| ImGui_ImplGlfw_InitForOpenGL(window, true); | |
| ImGui_ImplOpenGL3_Init(glsl_version); | |
| SetupLcdTexture(&lcd_texture); | |
| return OkStatus(); | |
| } | |
| void DisplayDriverImgUI::RecreateLcdTexture() { | |
| if (old_lcd_texture_display_mode_nearest != | |
| lcd_texture_display_mode_nearest) { | |
| old_lcd_texture_display_mode_nearest = lcd_texture_display_mode_nearest; | |
| SetupLcdTexture(&lcd_texture); | |
| } | |
| } | |
| void DisplayDriverImgUI::Render() { | |
| UpdateLcdTexture(); | |
| // Poll and handle events (inputs, window resize, etc.) | |
| glfwPollEvents(); | |
| touchscreen_mouse_position.left_button_pressed = false; | |
| double mouse_xpos = 0, mouse_ypos = 0; | |
| glfwGetCursorPos(window, &mouse_xpos, &mouse_ypos); | |
| if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { | |
| touchscreen_mouse_position.left_button_pressed = true; | |
| } | |
| // Start the Dear ImGui frame | |
| ImGui_ImplOpenGL3_NewFrame(); | |
| ImGui_ImplGlfw_NewFrame(); | |
| ImGui::NewFrame(); | |
| ImGuiIO& io = ImGui::GetIO(); | |
| int display_w, display_h; | |
| glfwGetFramebufferSize(window, &display_w, &display_h); | |
| // Build the empty dockspace background | |
| static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None; | |
| ImGuiWindowFlags window_flags = | |
| ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; | |
| const ImGuiViewport* viewport = ImGui::GetMainViewport(); | |
| ImGui::SetNextWindowPos(viewport->WorkPos); | |
| ImGui::SetNextWindowSize(viewport->WorkSize); | |
| ImGui::SetNextWindowViewport(viewport->ID); | |
| ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); | |
| ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); | |
| window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | | |
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; | |
| window_flags |= | |
| ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; | |
| // 0px padding around the dockspace | |
| ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); | |
| // Start docspace window | |
| ImGui::Begin("DockSpace", NULL, window_flags); | |
| // Create empty dockspace container | |
| ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); | |
| ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags); | |
| ImGui::PopStyleVar(); // Padding around docspace window | |
| ImGui::PopStyleVar(2); // WindowRounding and WindowBorderSize | |
| // Dockspace menu | |
| if (ImGui::BeginMenuBar()) { | |
| if (ImGui::BeginMenu("Options")) { | |
| ImGui::MenuItem("Show Imgui Demo Window", "", &show_imgui_demo_window); | |
| ImGui::Separator(); | |
| if (ImGui::MenuItem("Quit")) { | |
| CleanupAndExit(); | |
| } | |
| ImGui::EndMenu(); | |
| } | |
| HelpMarker( | |
| "Window Docking: " | |
| "\n" | |
| "- Drag from window title bar or their tab to dock/undock." | |
| "\n" | |
| "- Drag from window menu button (upper-left button) to undock an " | |
| "entire node (all windows)." | |
| "\n" | |
| "- Hold SHIFT to disable docking"); | |
| ImGui::EndMenuBar(); | |
| } | |
| ImGui::End(); // end dockspace | |
| // 1. Show the big demo window (Most of the sample code is in | |
| // ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear | |
| // ImGui!). | |
| if (show_imgui_demo_window) { | |
| ImGui::ShowDemoWindow(&show_imgui_demo_window); | |
| } | |
| // Draw Screen(s) | |
| // Screen 1 Window | |
| ImGui::Begin("Screen 1"); | |
| // Calculate the display texture draw coordinates | |
| int scaled_width = kDisplayWidth * lcd_texture_display_scale; | |
| int scaled_height = kDisplayHeight * lcd_texture_display_scale; | |
| ImVec2 mouse_pos = ImGui::GetCursorScreenPos(); | |
| ImVec2 mouse_coordinates_of_base_image; | |
| ImGui::Image((void*)(intptr_t)lcd_texture, | |
| ImVec2(scaled_width, scaled_height), | |
| // Top left texure coord | |
| ImVec2(0.0f, 0.0f), | |
| // Lower right texture coord | |
| ImVec2(1.0f, 1.0f), | |
| // Tint (none applied) | |
| ImVec4(1.0f, 1.0f, 1.0f, 1.0f), | |
| // Border color (50% white) | |
| ImVec4(1.0f, 1.0f, 1.0f, 0.5f)); | |
| mouse_coordinates_of_base_image.x = | |
| floor((io.MousePos.x - mouse_pos.x) / lcd_texture_display_scale); | |
| mouse_coordinates_of_base_image.y = | |
| floor((io.MousePos.y - mouse_pos.y) / lcd_texture_display_scale); | |
| if (ImGui::IsItemHovered()) { | |
| ImGui::BeginTooltip(); | |
| ImGui::Text("mouse coords = %.1f, %.1f", | |
| mouse_coordinates_of_base_image.x, | |
| mouse_coordinates_of_base_image.y); | |
| ImGui::EndTooltip(); | |
| } | |
| ImGui::End(); // Screen 1 Window | |
| // For reference this forces the "Screen 1" window to be docked to the main | |
| // dockspace: | |
| // ImGui::DockBuilderDockWindow("Screen 1", dockspace_id); | |
| ImGui::Begin("Screen 1 Settings"); | |
| ImGui::Text("Pixel Size = %d x %d", kDisplayWidth, kDisplayHeight); | |
| ImGui::SliderInt("Integer Scaling", &lcd_texture_display_scale, 1, 10); | |
| ImGui::Checkbox("Nearest neighbor", &lcd_texture_display_mode_nearest); | |
| ImGui::Separator(); | |
| touchscreen_mouse_position.position_x = mouse_coordinates_of_base_image.x; | |
| touchscreen_mouse_position.position_y = mouse_coordinates_of_base_image.y; | |
| ImGui::Text("Mouse position = %d, %d", | |
| touchscreen_mouse_position.position_x, | |
| touchscreen_mouse_position.position_y); | |
| ImGui::Text("Mouse Left button pressed: %d", | |
| touchscreen_mouse_position.left_button_pressed); | |
| // Demo Window toggle | |
| ImGui::Separator(); | |
| ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", | |
| 1000.0f / ImGui::GetIO().Framerate, | |
| ImGui::GetIO().Framerate); | |
| ImGui::End(); | |
| // Done building the UI | |
| ImGui::Render(); | |
| glViewport(0, 0, display_w, display_h); | |
| glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| // Render ImGui | |
| ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); | |
| glfwSwapBuffers(window); | |
| if (glfwWindowShouldClose(window)) { | |
| CleanupAndExit(); | |
| } | |
| } | |
| void DisplayDriverImgUI::WriteFramebuffer(Framebuffer framebuffer, | |
| WriteCallback write_callback) { | |
| PW_ASSERT(framebuffer.is_valid()); | |
| PW_ASSERT(framebuffer.pixel_format() == PixelFormat::RGB565); | |
| RecreateLcdTexture(); | |
| FramebufferReader reader(framebuffer); | |
| // Copy frame_buffer into lcd_pixel_data | |
| for (GLuint x = 0; x < kDisplayWidth; x++) { | |
| for (GLuint y = 0; y < kDisplayHeight; y++) { | |
| if (auto c = reader.GetPixel(x, y); c.ok()) { | |
| _SetTexturePixel(x, y, c.value()); | |
| } | |
| } | |
| } | |
| Render(); | |
| write_callback(std::move(framebuffer), OkStatus()); | |
| } | |
| Status DisplayDriverImgUI::WriteRow(span<uint16_t> row_pixels, | |
| uint16_t row_idx, | |
| uint16_t col_idx) { | |
| RecreateLcdTexture(); | |
| for (auto c : row_pixels) { | |
| _SetTexturePixel(col_idx++, row_idx, c); | |
| } | |
| // Rendering here is horribly slow - come up with better solution. | |
| Render(); | |
| return OkStatus(); | |
| } | |
| uint16_t DisplayDriverImgUI::GetWidth() const { return kDisplayWidth; } | |
| uint16_t DisplayDriverImgUI::GetHeight() const { return kDisplayHeight; } | |
| GLFWwindow* DisplayDriverImgUI::GetGlfwWindow() { return window; } | |
| ImGuiMousePosition DisplayDriverImgUI::GetImGuiMousePosition() { | |
| return touchscreen_mouse_position; | |
| } | |
| } // namespace pw::display_driver |