10#include <imgui_impl_glfw.h>
11#include <imgui_impl_wgpu.h>
12#include <imgui_internal.h>
15#include <sigc++/adaptors/bind.h>
22#include "objects/Project.hpp"
23#include "objects/Sensor.hpp"
25#include "panels/ChannelMappingPanel.hpp"
26#include "panels/MenuPanel.hpp"
27#include "panels/ProjectPanel.hpp"
43 static_cast<int>(viewport_width),
44 static_cast<int>(viewport_height)
47#if !defined(__EMSCRIPTEN__) || defined(__DOXYGEN__)
51 dockspace_id(ImHashStr(
"MainDockSpace"))
53 setup_subscriptions();
55 static constexpr auto timed_wait_any = wgpu::InstanceFeatureName::TimedWaitAny;
56 constexpr wgpu::InstanceDescriptor instance_desc{.requiredFeatureCount = 1, .requiredFeatures = &timed_wait_any};
58 instance = wgpu::CreateInstance(&instance_desc);
61 int actual_height = 0;
62 glfwGetFramebufferSize(window, &actual_width, &actual_height);
63 assert(actual_width >= 0);
64 assert(actual_height >= 0);
68 surface = SurfaceFactory::create_surface(instance, window);
70 if (
const auto adapter_wait = instance.WaitAny(request_adapter(), operation_timeout);
71 adapter_wait != wgpu::WaitStatus::Success || !adapter)
72 throw ConfigurationError(
"WebGPU adapter request did not complete");
74 if (
const auto device_wait = instance.WaitAny(request_device(), operation_timeout);
75 device_wait != wgpu::WaitStatus::Success || !device)
76 throw ConfigurationError(
"WebGPU device request did not complete");
78 surface.GetCapabilities(adapter, &surface_capabilities);
79 configure_surface(surface, device, surface_capabilities, viewport_width, viewport_height);
93 if (ImGui::GetCurrentContext() !=
nullptr) {
94 ImGui_ImplWGPU_Shutdown();
95 ImGui_ImplGlfw_Shutdown();
96 ImPlot3D::DestroyContext();
97 ImPlot::DestroyContext();
98 ImGui::DestroyContext();
102 surface.Unconfigure();
110 if (window !=
nullptr) {
111 glfwDestroyWindow(window);
128 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
130 auto*
const window = glfwCreateWindow(width, height,
"EchoMap",
nullptr,
nullptr);
132 if (window ==
nullptr) {
141 const wgpu::Surface& surface,
142 const wgpu::Device& device,
143 const wgpu::SurfaceCapabilities& capabilities,
148 const wgpu::SurfaceConfiguration config{
152 .format = capabilities.formats !=
nullptr ? *capabilities.formats : wgpu::TextureFormat::RGBA8Snorm,
153 .usage = wgpu::TextureUsage::RenderAttachment,
154 .width = viewport_width,
155 .height = viewport_height,
159 capabilities.alphaModes !=
nullptr ? *capabilities.alphaModes : wgpu::CompositeAlphaMode::Opaque,
160 .presentMode = wgpu::PresentMode::Fifo,
163 surface.Configure(&config);
170 despatcher.load_project_finished_channel.nominate_consumer(
175 despatcher.load_signal_file_channel.nominate_consumer(
181 raise_error(error.what());
182 LOG_F_ERROR(
"Error modal raised due to error: {}", error.what());
188 const wgpu::RequestAdapterOptions options{.compatibleSurface = surface};
190 return instance.RequestAdapter(
192 wgpu::CallbackMode::WaitAnyOnly,
193 [
this](
const wgpu::RequestAdapterStatus status, wgpu::Adapter new_adapter,
const wgpu::StringView message) {
194 if (status != wgpu::RequestAdapterStatus::Success)
196 Logger::Level::Error,
198 "WebGPU adapter request failed {}: {}.",
199 std::to_underlying(status),
203 adapter = std::move(new_adapter);
210 wgpu::DeviceDescriptor desc{};
211 desc.SetDeviceLostCallback(
212 wgpu::CallbackMode::AllowSpontaneous,
213 [](
const wgpu::Device&,
const wgpu::DeviceLostReason reason,
const wgpu::StringView message) {
214 if (reason == wgpu::DeviceLostReason::Destroyed)
218 Logger::Level::Warning,
220 "Device lost because of reason {}: {}.",
221 std::to_underlying(reason),
227 desc.SetUncapturedErrorCallback(
228 [](
const wgpu::Device&,
const wgpu::ErrorType error_type,
const wgpu::StringView message) {
230 Logger::Level::Error,
232 "WebGPU error {}: {}",
233 std::to_underlying(error_type),
239 return adapter.RequestDevice(
241 wgpu::CallbackMode::WaitAnyOnly,
242 [
this](
const wgpu::RequestDeviceStatus status, wgpu::Device new_device,
const wgpu::StringView message) {
243 if (status != wgpu::RequestDeviceStatus::Success)
245 Logger::Level::Error,
247 "WebGPU device request failed: {}",
251 device = std::move(new_device);
266 wgpu::SurfaceTexture surface_texture{};
267 surface.GetCurrentTexture(&surface_texture);
269 switch (surface_texture.status) {
270 case wgpu::SurfaceGetCurrentTextureStatus::Error:
271 case wgpu::SurfaceGetCurrentTextureStatus::Lost:
272 case wgpu::SurfaceGetCurrentTextureStatus::Outdated:
273 surface.Unconfigure();
274 configure_surface(surface, device, surface_capabilities, viewport_width, viewport_height);
277 case wgpu::SurfaceGetCurrentTextureStatus::Timeout:
280 case wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal:
281 case wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal:
285 const wgpu::TextureView surface_view = surface_texture.texture.CreateView();
287 ImGui_ImplWGPU_NewFrame();
288 ImGui_ImplGlfw_NewFrame();
294 for (
const auto& panel :
panels)
306 const wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
311 wgpu::RenderPassColorAttachment
const attachment{
312 .view = surface_view,
313 .loadOp = wgpu::LoadOp::Clear,
314 .storeOp = wgpu::StoreOp::Store,
315 .clearValue = {.r = 0.1, .g = 0.1, .b = 0.1, .a = 1.0},
318 const wgpu::RenderPassDescriptor pass_descriptor{.colorAttachmentCount = 1, .colorAttachments = &attachment};
319 const wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&pass_descriptor);
321 ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), pass.Get());
324 const wgpu::CommandBuffer commands = encoder.Finish();
327 device.GetQueue().Submit(1, &commands);
329#if !defined(__EMSCRIPTEN__) || defined(__DOXYGEN__)
342 IMGUI_CHECKVERSION();
343 ImGui::CreateContext();
344 ImPlot::CreateContext();
345 ImPlot3D::CreateContext();
347 auto& io = ImGui::GetIO();
348 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
349 io.Fonts->AddFontFromMemoryCompressedTTF(
351 static_cast<const void*
>(data::RobotoMedium_compressed_data),
352 std::size(data::RobotoMedium_compressed_data),
355 ImGui::StyleColorsLight();
357 if (!ImGui_ImplGlfw_InitForOther(window,
true))
361 ImGui_ImplWGPU_InitInfo init_info{};
362 init_info.Device = device.Get();
365 init_info.RenderTargetFormat =
static_cast<WGPUTextureFormat
>(std::to_underlying(
366 (surface_capabilities.formats !=
nullptr) ? *surface_capabilities.formats : wgpu::TextureFormat::RGBA8Snorm
369 if (!ImGui_ImplWGPU_Init(&init_info))
372#if defined(__EMSCRIPTEN__) || defined(__DOXYGEN__)
378 ImGui_ImplGlfw_InstallEmscriptenCallbacks(window,
"#canvas");
382void EchoMap::setup_dockspace()
384 const auto*
const viewport = ImGui::GetMainViewport();
386 if (!dockspace_configured) {
387 dockspace_configured =
true;
389 if (ImGui::DockBuilderGetNode(dockspace_id) ==
nullptr) {
391 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
392 ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
395 ImGuiID dock_id_left = 0;
396 ImGuiID dock_id_main = 0;
397 ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, .15f, &dock_id_left, &dock_id_main);
398 ImGui::DockBuilderDockWindow(ProjectPanel::get_imgui_stable_name(), dock_id_left);
401 ImGuiID dock_id_main_upper = 0;
402 ImGuiID dock_id_main_lower = 0;
403 ImGui::DockBuilderSplitNode(dock_id_main, ImGuiDir_Up, .33f, &dock_id_main_upper, &dock_id_main_lower);
406 ImGuiID dock_id_main_upper_left = 0;
407 ImGuiID dock_id_main_upper_right = 0;
408 ImGui::DockBuilderSplitNode(
412 &dock_id_main_upper_left,
413 &dock_id_main_upper_right
415 ImGui::DockBuilderDockWindow(ChannelMappingPanel::get_imgui_stable_name(), dock_id_main_upper_left);
416 ImGui::DockBuilderDockWindow(SensorGeometryPanel::get_imgui_stable_name(), dock_id_main_upper_right);
419 ImGuiID dock_id_main_lower_left = 0;
420 ImGuiID dock_id_main_lower_right = 0;
421 ImGui::DockBuilderSplitNode(
425 &dock_id_main_lower_left,
426 &dock_id_main_lower_right
428 ImGui::DockBuilderDockWindow(SignalWaveformPanel::get_imgui_stable_name(), dock_id_main_lower_left);
429 ImGui::DockBuilderDockWindow(SignalDFTPanel::get_imgui_stable_name(), dock_id_main_lower_right);
431 ImGui::DockBuilderFinish(dockspace_id);
435 ImGui::DockSpaceOverViewport(dockspace_id, viewport, ImGuiDockNodeFlags_PassthruCentralNode);
443 glfwGetFramebufferSize(window, &fb_width, &fb_height);
445 if (fb_width <= 0 || fb_height <= 0)
451 const auto new_height =
static_cast<std::uint32_t>(fb_height);
453 if (new_width != viewport_width || new_height != viewport_height) {
456 viewport_width = new_width;
457 viewport_height = new_height;
459 surface.Unconfigure();
460 configure_surface(surface, device, surface_capabilities, viewport_width, viewport_height);
472 while (!notification_queue.empty()) {
473 auto& notification = notification_queue.back();
475 auto*
const hint =
static_cast<void*
>(¬ification);
477 LOG_F_DEBUG(
"Consuming {} with hint {}.", type_name, hint);
482 LOG_F_WARN(
"{} with hint {} was dropped: {}", type_name, hint, warning.
what());
484 raise_error(exception.
what());
485 LOG_F_ERROR(
"{} with hint {} was responsible for error: {}", type_name, hint, exception.
what());
488 notification_queue.pop_back();
494 while (
auto result =
worker.try_get_result())
502void EchoMap::raise_error(
506 error_modal.emplace(message, [
this] {
511void EchoMap::raise_error(
516 error_modal.emplace(message, exception, [
this] {
517 notify(ClearErrorNotification{});
521void EchoMap::handle_notification(
522 const AddChannelMappingNotification& notification
525 notification.verify_project(project.get());
526 project->add_association(notification.signal_id, notification.sensor_id);
529void EchoMap::handle_notification(
530 const ModifySensorColourNotification& notification
533 notification.verify_project(project.get());
534 project->get_mutable_sensor(notification.sensor_id).set_colour(notification.colour);
537void EchoMap::handle_notification(
538 const ModifySensorPositionNotification& notification
541 notification.verify_project(project.get());
542 project->get_mutable_sensor(notification.sensor_id).set_position(notification.position);
545void EchoMap::handle_notification(
546 const ProjectSelectionCompleteNotification& notification
549 active_modal.reset();
551 if (notification.path.has_value())
555void EchoMap::handle_notification(
556 const ClearErrorNotification& notification
559 std::ignore = notification;
563void EchoMap::handle_result(
564 LoadProjectResult&& result
567 if (active_modal !=
nullptr) {
568 LOG_WARN(
"Ignoring request to change active Project since there is an active modal.");
572 change_active_project(std::move(std::move(result).take_project()));
575void EchoMap::handle_result(
576 LoadSignalFileResult&& result
579 if (project ==
nullptr || result.get_project_id() != project->get_id())
581 "Dropping LoadSignalFileResult, which was intended for the unavailable Project with ID {}.",
582 result.get_project_id()
585 for (
auto&& signals = std::move(result).take_signals();
auto signal : signals | std::views::as_rvalue)
586 project->add_signal(std::move(signal));
589void EchoMap::change_active_project(
593 if (new_project ==
nullptr)
594 LOG_DEBUG(
"Clearing the active project.");
596 LOG_F_DEBUG(
"Changing active project to {}.", new_project->get_name());
598 for (
const auto& panel : panels)
599 panel->change_active_project(new_project.get());
601 project = std::move(new_project);
608 notification_queue.emplace_back(notification);
615 "Scheduling {} with hint {} at position {}.",
617 static_cast<void*
>(¬ification_queue.back()),
618 notification_queue.size() - 1
623 const unsigned int count
626 forced_frames += count;
AllNotifications specification.
EchoMap ConfigurationError exception specification.
EchoMap class specification.
IgnoredWarning specification.
Project-loading task specification.
EchoMap portable logger specification.
#define LOG_F_DEBUG(msg,...)
Conditionally logs a formatted debug-level message using echomap::Logger::log_f.
#define LOG_DEBUG(msg,...)
Conditionally logs an unformatted debug-level message using echomap::Logger::log.
#define LOG_F_WARN(msg,...)
Logs a formatted warning-level message using echomap::Logger::log_f.
#define LOG_F_ERROR(msg,...)
Logs a formatted error-level message using echomap::Logger::log_f.
#define LOG_WARN(msg)
Logs an unformatted warning-level message using echomap::Logger::log.
PartialProject specification.
Roboto Medium font as a C-style array.
EchoMap sensor geometry panel specification.
SignalDFTPanel specification.
Audio signal class specification.
EchoMap WebGPU-GLFW Surface factory specification.
A ConfigurationError indicates an error encountered during the initial configuration of the EchoMap g...
void increment_forced_frames(unsigned int count=4) noexcept
Indicate to the renderer that the following frames should always be rendered, regardless of whether t...
void notify(const Notification ¬ification)
Submit a new Notification to the application queue.
std::vector< sigc::scoped_connection > connections
RAII lifetime manager for signal connections.
EchoMap()
Initialise a EchoMap application instance.
void setup_subscriptions()
Configure the core signals for the application instance.
void render() noexcept
Perform a render cycle on the configured Surface and Device.
wgpu::Future request_device() noexcept
Produce a WebGPU Future for requesting an accelerator device.
std::unique_ptr< IPanel > active_modal
The current active non-ErrorModal modal panel.
virtual void visit_notification(Notification ¬ification)=0
Uses std::visit on the given notification to invoke the corresponding handler.
static void configure_surface(const wgpu::Surface &surface, const wgpu::Device &device, const wgpu::SurfaceCapabilities &capabilities, std::uint32_t viewport_width, std::uint32_t viewport_height) noexcept
Configure a WebGPU Surface from a static context given metadata and Adapter capabilities.
void process_notifications()
Handle any unconsumed Notification objects from the queue.
bool handle_window_resize() noexcept
Check if the window has been resized compared with the stored dimensions, updating member variables a...
static GLFWwindow * create_window(int width, int height)
Create a new GLFW window of the specified dimensions from a static context.
std::vector< std::unique_ptr< IProjectPanel > > panels
Individual display components.
void setup_imgui()
Create a context for Dear ImGui and ImPlot, and configure the plain GLFW and WebGPU backends.
void process_worker_results()
Handle any unconsumed events from the Worker.
wgpu::Future request_adapter() noexcept
Produce a WebGPU Future for requesting an Adapter.
Worker worker
Multi-threaded worker for scheduling heavy computation tasks.
std::optional< ErrorModal > error_modal
Persistent panel to indicate errors over all other panels.
virtual ~EchoMap() noexcept
Clean up all persistent state registered by the application instance.
WorkerResultDespatcher despatcher
Despatcher to manage Worker result channels.
Indicates that an ITask did not successfully complete.
Exception class to indicate a non-fatal warning indicating an exceptional case of dropping some notif...
Denotes a loaded Project completed by a LoadProjectTask job.
static void log(Level level, std::string_view message, std::source_location location=std::source_location::current())
Log an unformatted message to the backend.
static void log_f(const Level level, const std::source_location location, std::format_string< Args... > fmt, Args &&... args)
Log a formatted message to the backend with a specific source location.
std::variant< AddChannelMappingNotification, ModifySensorColourNotification, ModifySensorPositionNotification, ProjectSelectionCompleteNotification, ClearErrorNotification, RaiseFileChooserNotification, RegisterVFSMappingNotification, CompleteProjectLoadNotification, CancelProjectLoadNotification > Notification
A Notification is a trivial message sent exclusively to the EchoMap controller.
The main EchoMap outermost namespace for all non-exported symbols.
A notification indicating that any error state visible to the user should be reset.
static const std::array< std::string_view, std::variant_size_v< Notification > > indexed_names
Aggregated names by the index of the alternative.