Skip to content

Commit

Permalink
Place IMGUI UIs into a separate common module (#25131)
Browse files Browse the repository at this point in the history
* Modularize imgui - split windows and ready to add into any app

* Comment update

* Added extra assert

* Switch to using a work scheduler instead of a stack lock for state updates

* Restyle

* typo fix

* Update to sem_wait

* Restyle

* Generate shutdown event before stoppign the event loop task

* Restyle

* Update the code to not reference Server (would not compile otherwise)

* Use a semaphore to wait for UI to send the shutdown. Also update comments

* Restyle

---------

Co-authored-by: Andrei Litvin <[email protected]>
  • Loading branch information
2 people authored and pull[bot] committed Jan 8, 2024
1 parent daafb7d commit 4015190
Show file tree
Hide file tree
Showing 12 changed files with 844 additions and 558 deletions.
41 changes: 41 additions & 0 deletions examples/common/imgui_ui/BUILD.gn
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) 2023 Project CHIP 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
#
# http://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.
import("//build_overrides/chip.gni")

import("${chip_root}/build/chip/tools.gni")
import("${chip_root}/third_party/imgui/imgui.gni")

config("imgui_ui_config") {
# allow including via 'imgui_ui/....'
include_dirs = [ "../" ]
}

static_library("imgui_ui") {
sources = [
"ui.cpp",
"ui.h",
]

deps = [
"${chip_root}/examples/common/imgui_ui/windows",
"${chip_root}/examples/platform/linux:app-main",
"${chip_root}/src/lib/support",
"${chip_root}/third_party/imgui",
]

public_configs = [
":imgui_ui_config",
"${chip_root}/third_party/imgui:imgui_config", # propagate enabling flag
]
}
230 changes: 230 additions & 0 deletions examples/common/imgui_ui/ui.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
*
* Copyright (c) 2023 Project CHIP 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
*
* http://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.
*/
#include "ui.h"

#include <SDL.h>
#include <SDL_opengl.h>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_sdl2.h>

#include <lib/support/logging/CHIPLogging.h>

#include <atomic>
#include <thread>

namespace example {
namespace Ui {
namespace {

// Controls running the UI event loop
std::atomic<bool> gUiRunning{ false };

void UiInit(SDL_GLContext * gl_context, SDL_Window ** window)
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0)
{
ChipLogError(AppServer, "SDL Init Error: %s\n", SDL_GetError());
return;
}

#if defined(__APPLE__)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac
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, 2);
#else
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, 0);
#endif

#ifdef SDL_HINT_IME_SHOW_UI
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
#endif

SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
*window = SDL_CreateWindow("Light UI", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, window_flags);
*gl_context = SDL_GL_CreateContext(*window);
SDL_GL_MakeCurrent(*window, *gl_context);
SDL_GL_SetSwapInterval(1); // Enable vsync

// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO & io = ImGui::GetIO();
(void) io;
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls

ImGui::StyleColorsDark();

// Setup Platform/Renderer backends
ImGui_ImplSDL2_InitForOpenGL(*window, *gl_context);
ImGui_ImplOpenGL3_Init();
}

void UiShutdown(SDL_GLContext * gl_context, SDL_Window ** window)
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();

SDL_GL_DeleteContext(*gl_context);
SDL_DestroyWindow(*window);
SDL_Quit();
}

void EventLoop(ImguiUi * ui)
{
gUiRunning = true;
SDL_GLContext gl_context;
SDL_Window * window = nullptr;

UiInit(&gl_context, &window);

ImGuiIO & io = ImGui::GetIO();

while (gUiRunning.load())
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL2_ProcessEvent(&event);
if ((event.type == SDL_QUIT) ||
(event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE &&
event.window.windowID == SDL_GetWindowID(window)))
{
gUiRunning = false;
}
}

ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();

ui->UpdateState();
ui->Render();

// rendering
ImGui::Render();
glViewport(0, 0, (int) io.DisplaySize.x, (int) io.DisplaySize.y);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}

UiShutdown(&gl_context, &window);

ChipLogProgress(AppServer, "UI thread Stopped...");
}

} // namespace

void ImguiUi::RunMainLoop()
{
// Guaranteed to be on the main task (no chip event loop started yet)
ChipLoopLoadInitialState();

// Platform event loop will be on a separate thread,
// while the event UI loop will be on the main thread.
chip::DeviceLayer::PlatformMgr().StartEventLoopTask();

// SignalSafeStopMainLoop will stop this loop below
// or the loop exits by itself when processing a SDL
// exit (generally by clicking the window close icon).
EventLoop(this);

// ensure shutdown events are generated (generally basic cluster
// will send a shutdown event to subscribers).
//
// We attempt to wait for finish as the event will be sent sync.
// Since the Main loop is stopped, there will be no MRP, however at least
// one event is attempted to be sent.
chip::DeviceLayer::PlatformMgr().ScheduleWork(
[](intptr_t arg) {
chip::DeviceLayer::PlatformMgr().HandleServerShuttingDown();
sem_t * semaphore = reinterpret_cast<sem_t *>(arg);
sem_post(semaphore); // notify complete
},
reinterpret_cast<intptr_t>(&mChipLoopWaitSemaphore));
sem_wait(&mChipLoopWaitSemaphore);

// Stop the chip main loop as well. This is expected to
// wait for the task to finish.
chip::DeviceLayer::PlatformMgr().StopEventLoopTask();
}

void ImguiUi::SignalSafeStopMainLoop()
{
gUiRunning = false;
}

void ImguiUi::ChipLoopStateUpdate()
{
assertChipStackLockedByCurrentThread();
for (auto it = mWindows.begin(); it != mWindows.end(); it++)
{
(*it)->UpdateState();
}
}

void ImguiUi::ChipLoopLoadInitialState()
{
assertChipStackLockedByCurrentThread();
for (auto it = mWindows.begin(); it != mWindows.end(); it++)
{
(*it)->LoadInitialState();
}
}

void ImguiUi::Render()
{
for (auto it = mWindows.begin(); it != mWindows.end(); it++)
{
(*it)->Render();
}
}

void ImguiUi::ChipLoopUpdateCallback(intptr_t self)
{
ImguiUi * _this = reinterpret_cast<ImguiUi *>(self);
_this->ChipLoopStateUpdate();
sem_post(&_this->mChipLoopWaitSemaphore); // notify complete
}

void ImguiUi::UpdateState()
{
CHIP_ERROR err = chip::DeviceLayer::PlatformMgr().ScheduleWork(&ChipLoopUpdateCallback, reinterpret_cast<intptr_t>(this));

if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "Failed to schedule state update: %" CHIP_ERROR_FORMAT, err.Format());
return;
}

// ensure update is done when exiting
sem_wait(&mChipLoopWaitSemaphore);
}

} // namespace Ui
} // namespace example
65 changes: 65 additions & 0 deletions examples/common/imgui_ui/ui.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
*
* Copyright (c) 2023 Project CHIP 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
*
* http://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.
*/
#pragma once

#include <AppMain.h>
#include <imgui_ui/windows/window.h>

#include <semaphore.h>

#include <list>
#include <memory>

namespace example {
namespace Ui {

/**
* Supports showing a UI using ImGUI
*
* The UI supports several windows, such as QR codes or device control.
*/
class ImguiUi : public AppMainLoopImplementation
{
public:
ImguiUi() { sem_init(&mChipLoopWaitSemaphore, 0 /* shared */, 0); }
virtual ~ImguiUi() { sem_destroy(&mChipLoopWaitSemaphore); }

void AddWindow(std::unique_ptr<Window> window) { mWindows.push_back(std::move(window)); }

void UpdateState(); // runs a state update from ember/app
void Render(); // render windows to screen

// AppMainLoopImplementation
void RunMainLoop() override;
void SignalSafeStopMainLoop() override;

private:
// First initial state load
void ChipLoopLoadInitialState();

// Updates the window states. Run in the CHIP main loop (has access
// to CHIP API calls)
void ChipLoopStateUpdate();

sem_t mChipLoopWaitSemaphore;
std::list<std::unique_ptr<Window>> mWindows;

static void ChipLoopUpdateCallback(intptr_t self);
};

} // namespace Ui
} // namespace example
55 changes: 55 additions & 0 deletions examples/common/imgui_ui/windows/BUILD.gn
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) 2023 Project CHIP 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
#
# http://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.
import("//build_overrides/chip.gni")

import("${chip_root}/build/chip/tools.gni")
import("${chip_root}/third_party/imgui/imgui.gni")

source_set("windows") {
sources = [ "window.h" ]
}

static_library("qrcode") {
sources = [
"qrcode.cpp",
"qrcode.h",
]

deps = [
":windows",
"${chip_root}/examples/common/QRCode",
"${chip_root}/examples/platform/linux:app-main",
"${chip_root}/src/app/server",
"${chip_root}/src/setup_payload",
"${chip_root}/third_party/imgui",
]

public_configs =
[ "${chip_root}/examples/common/QRCode:qrcode-common_config" ]
}

static_library("light") {
sources = [
"light.cpp",
"light.h",
]

deps = [
":windows",
"${chip_root}/src/app/common:cluster-objects",
"${chip_root}/third_party/imgui",
]

public_configs = [ "${chip_root}/src:includes" ]
}
Loading

0 comments on commit 4015190

Please sign in to comment.