yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
controller.cc
Go to the documentation of this file.
1#include "controller.h"
2
3#include "app/application.h"
5
6#if defined(__APPLE__)
7#include <TargetConditionals.h>
8#endif
9
10#include <chrono>
11#include <cstdio>
12#include <cstdlib>
13#include <string>
14#include <vector>
15
16#include "absl/status/status.h"
21#include "app/emu/emulator.h"
28#include "app/platform/timing.h"
30#include "imgui/imgui.h"
31#include "imgui/imgui_internal.h"
33#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
35#include "imgui_test_engine/imgui_te_engine.h"
36#endif
37#if defined(__APPLE__) && \
38 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
40#endif
41
42namespace yaze {
43
44namespace {
45
46// ---------------------------------------------------------------------------
47// Input diagnostics (off unless YAZE_DEBUG_INPUT_LOG is set).
48//
49// YAZE_DEBUG_INPUT_LOG=<path> appends one line per SDL mouse/key/window event
50// and one line per frame in which ImGui's mouse state changed: buttons down,
51// clicked/released, hovered window and ID, active ID, key mods and the open
52// popup stack. Used to find where a click is lost between SDL and ImGui.
53//
54// YAZE_DEBUG_INPUT_SCRIPT=<path> replays "<ms> <action> <x> <y>" lines
55// (action: move, ldown, lup, rdown, rup) through SDL_PushEvent, relative to
56// the first frame, so a hidden instance can be clicked through the real SDL
57// queue, event filter and ImGui SDL backend.
58// ---------------------------------------------------------------------------
59struct InputDebug {
60 FILE* log = nullptr;
61 struct Step {
62 int at_ms;
63 std::string action;
64 float x;
65 float y;
66 };
67 std::vector<Step> script;
68 size_t next_step = 0;
69 std::chrono::steady_clock::time_point start;
70 bool started = false;
71 std::string last_frame_line;
72};
73
75 static InputDebug* debug = [] {
76 auto* d = new InputDebug();
77 if (const char* path = std::getenv("YAZE_DEBUG_INPUT_LOG");
78 path != nullptr && path[0] != '\0') {
79 d->log = std::fopen(path, "a");
80 }
81 if (const char* path = std::getenv("YAZE_DEBUG_INPUT_SCRIPT");
82 path != nullptr && path[0] != '\0') {
83 if (FILE* f = std::fopen(path, "r")) {
84 char action[32];
85 int at = 0;
86 float x = 0, y = 0;
87 while (std::fscanf(f, "%d %31s %f %f", &at, action, &x, &y) == 4) {
88 d->script.push_back({at, action, x, y});
89 }
90 std::fclose(f);
91 }
92 }
93 return d;
94 }();
95 return *debug;
96}
97
98double InputDebugMs() {
99 auto& d = GetInputDebug();
100 return std::chrono::duration<double, std::milli>(
101 std::chrono::steady_clock::now() - d.start)
102 .count();
103}
104
105#ifndef YAZE_USE_SDL3
106void InputDebugLogSdlEvent(const SDL_Event& e) {
107 auto& d = GetInputDebug();
108 if (d.log == nullptr) {
109 return;
110 }
111 switch (e.type) {
112 case SDL_MOUSEBUTTONDOWN:
113 case SDL_MOUSEBUTTONUP:
114 std::fprintf(d.log, "%9.1f sdl %s btn=%d x=%d y=%d win=%u clicks=%d\n",
115 InputDebugMs(),
116 e.type == SDL_MOUSEBUTTONDOWN ? "mousedown" : "mouseup",
117 e.button.button, e.button.x, e.button.y, e.button.windowID,
118 e.button.clicks);
119 break;
120 case SDL_KEYDOWN:
121 case SDL_KEYUP:
122 std::fprintf(d.log, "%9.1f sdl %s sym=0x%x mod=0x%x\n", InputDebugMs(),
123 e.type == SDL_KEYDOWN ? "keydown" : "keyup",
124 e.key.keysym.sym, e.key.keysym.mod);
125 break;
126 case SDL_WINDOWEVENT:
127 std::fprintf(d.log, "%9.1f sdl window event=%d\n", InputDebugMs(),
128 e.window.event);
129 break;
130 default:
131 break;
132 }
133}
134
136 auto& d = GetInputDebug();
137 if (d.next_step >= d.script.size()) {
138 return;
139 }
140 const ImGuiViewport* vp = ImGui::GetMainViewport();
141 const Uint32 window_id =
142 static_cast<Uint32>(reinterpret_cast<intptr_t>(vp->PlatformHandle));
143 while (d.next_step < d.script.size() &&
144 d.script[d.next_step].at_ms <= InputDebugMs()) {
145 const auto& step = d.script[d.next_step++];
146 SDL_Event ev{};
147 const int x = static_cast<int>(step.x);
148 const int y = static_cast<int>(step.y);
149 if (step.action == "move") {
150 ev.type = SDL_MOUSEMOTION;
151 ev.motion.windowID = window_id;
152 ev.motion.x = x;
153 ev.motion.y = y;
154 } else {
155 const bool down = step.action == "ldown" || step.action == "rdown";
156 ev.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
157 ev.button.windowID = window_id;
158 ev.button.button =
159 step.action[0] == 'r' ? SDL_BUTTON_RIGHT : SDL_BUTTON_LEFT;
160 ev.button.state = down ? SDL_PRESSED : SDL_RELEASED;
161 ev.button.clicks = 1;
162 ev.button.x = x;
163 ev.button.y = y;
164 }
165 SDL_PushEvent(&ev);
166 if (d.log != nullptr) {
167 std::fprintf(d.log, "%9.1f script %s %d %d\n", InputDebugMs(),
168 step.action.c_str(), x, y);
169 }
170 }
171}
172#endif // YAZE_USE_SDL3
173
175 auto& d = GetInputDebug();
176 if (d.log == nullptr) {
177 return;
178 }
179 const ImGuiContext& g = *ImGui::GetCurrentContext();
180 const ImGuiIO& io = g.IO;
181 std::string popups;
182 for (const ImGuiPopupData& p : g.OpenPopupStack) {
183 char id[48];
184 std::snprintf(id, sizeof(id), "(pending id=%08X parent=%08X f=%d)",
185 p.PopupId, p.OpenParentId, p.OpenFrameCount);
186 popups += p.Window ? p.Window->Name : id;
187 popups += "|";
188 }
189 char line[1024];
190 std::snprintf(
191 line, sizeof(line),
192 "down=%d%d%d clk=%d%d rel=%d%d pos=%.0f,%.0f hovwin=%s hovid=%08X "
193 "active=%08X(%s) nav=%s text=%d mods=0x%x focus=%d popups=[%s]",
194 io.MouseDown[0], io.MouseDown[1], io.MouseDown[2], io.MouseClicked[0],
195 io.MouseClicked[1], io.MouseReleased[0], io.MouseReleased[1],
196 io.MousePos.x, io.MousePos.y,
197 g.HoveredWindow ? g.HoveredWindow->Name : "-", g.HoveredIdPreviousFrame,
198 g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "-",
199 g.NavWindow ? g.NavWindow->Name : "-", io.WantTextInput ? 1 : 0,
200 static_cast<unsigned>(io.KeyMods), io.AppFocusLost ? 0 : 1,
201 popups.c_str());
202 const bool edge = io.MouseClicked[0] || io.MouseClicked[1] ||
203 io.MouseReleased[0] || io.MouseReleased[1];
204 if (edge || d.last_frame_line != line) {
205 std::fprintf(d.log, "%9.1f frame=%d %s\n", InputDebugMs(), g.FrameCount,
206 line);
207 std::fflush(d.log);
208 d.last_frame_line = line;
209 }
210}
211
212} // namespace
213
214absl::Status Controller::OnEntry(std::string filename) {
215 // Create window backend using factory (auto-selects SDL2 or SDL3)
217 auto renderer_type = gfx::RendererFactory::GetDefaultBackendType();
218
219 const auto& app_config = Application::Instance().GetConfig();
220
221 if (app_config.headless) {
222 LOG_INFO("Controller", "Using Null Window Backend (Headless Mode)");
224 renderer_type = gfx::RendererBackendType::Null;
225 }
226
227#if defined(__APPLE__) && \
228 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
230 renderer_type = gfx::RendererBackendType::Metal;
231#endif
232
233 // A second OnEntry() replaces the backend. Drop the waker first so no
234 // service thread can wake the backend that is about to be destroyed.
237 if (!window_backend_) {
238 return absl::InternalError("Failed to create window backend");
239 }
240
242 config.title = "Yet Another Zelda3 Editor";
243 config.resizable = true;
244 config.high_dpi =
245 false; // Disabled to match legacy behavior (SDL_WINDOW_RESIZABLE only)
246
247 if (app_config.service_mode) {
248 LOG_INFO("Controller", "Starting in Service Mode (Hidden Window)");
249 config.hidden = true;
250 }
251
252 RETURN_IF_ERROR(window_backend_->Initialize(config));
253
254 // From here on, service threads hand editor work to this thread through
255 // the queue instead of running it on their own threads. Binding before the
256 // rest of OnEntry means an early RPC waits for the first frame rather than
257 // racing initialization. The waker lets a posted task cut the idle wait.
258 auto& ui_queue = util::UiThreadQueue::Get();
259 ui_queue.BindToCurrentThread();
260 ui_queue.SetWaker(
261 [backend = window_backend_.get()]() { backend->WakeEventLoop(); });
262
263 // Create renderer via factory (auto-selects SDL2 or SDL3)
265 if (!window_backend_->InitializeRenderer(renderer_.get())) {
266 return absl::InternalError("Failed to initialize renderer");
267 }
268
269 // Initialize ImGui via backend (handles SDL2/SDL3 automatically)
270 RETURN_IF_ERROR(window_backend_->InitializeImGui(renderer_.get()));
271
272 // The test manager is linked at the controller/executable layer rather than
273 // the platform window library. Initialize its engine here, after ImGui has a
274 // context, so remote harness actions use the real engine. The dedicated GUI
275 // test runner owns a separate engine and initializes it below its OnEntry().
276#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE && \
277 !defined(YAZE_GUI_TEST_TARGET)
279#endif
280
281 // Initialize the graphics Arena with the renderer
283
284 // Set up audio for emulator (using backend's audio resources)
285 auto audio_buffer = window_backend_->GetAudioBuffer();
286 if (audio_buffer) {
287 editor_manager_.emulator().set_audio_buffer(audio_buffer.get());
288 }
289 editor_manager_.emulator().set_audio_device_id(
290 window_backend_->GetAudioDevice());
291
293 Application::Instance().GetConfig().asset_load_mode);
294
295 // Initialize editor manager with renderer
296 editor_manager_.Initialize(renderer_.get(), filename);
297
298 // imgui.ini loads on the first NewFrame; the layout section needs its
299 // handler registered before then.
301 mgr->RegisterLayoutIniHandler();
302 }
303
304 active_ = true;
305 return absl::OkStatus();
306}
307
308void Controller::SetStartupEditor(const std::string& editor_name,
309 const std::string& panels) {
310 // Process command-line flags for editor and panels
311 // Example: --editor=Dungeon --open_panels="dungeon.room_list,Room 0"
312 if (!editor_name.empty()) {
314 }
315}
316
318 if (!window_backend_)
319 return;
320
321 // Idle pacing. With nothing to animate and no input for a while, wait for
322 // the next event instead of drawing at the display rate: 10 fps after
323 // util::kMainLoopIdleAfter of quiet, 4 fps while the window is hidden or
324 // minimized (see util::MainLoopEventWaitMs). Any
325 // event (mouse move, key, window change) wakes the loop at once, and work
326 // that needs frames (emulator, music, texture uploads, UI tests,
327 // screenshots) keeps full rate. Queued UI-thread tasks also skip the wait;
328 // tasks posted during the wait end it through WakeEventLoop().
329 {
332 {
333 std::lock_guard<std::mutex> lock(screenshot_mutex_);
334 busy = busy || !screenshot_requests_.empty();
335 }
336#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
337 auto& tests = test::TestManager::Get();
338 busy = busy || tests.IsTestRunning();
339 // Harness RPCs queue engine tests that advance one frame at a time; a
340 // task stays in the queue until it finishes.
341 if (auto* engine = tests.GetUITestEngine()) {
342 busy = busy || !ImGuiTestEngine_IsTestQueueEmpty(engine);
343 }
344#endif
346 idle.busy = busy;
349 idle.quiet_for = std::chrono::steady_clock::now() - last_event_time_;
350 const int wait_ms = util::MainLoopEventWaitMs(idle);
351 if (!idle.busy && !idle.ui_tasks_pending) {
352 // Write batched log lines before going quiet, so they are on disk if
353 // the app then crashes or is killed.
355 }
356 if (wait_ms > 0) {
357 window_backend_->WaitForEvent(wait_ms);
358 }
359 }
360
361 {
362 auto& debug = GetInputDebug();
363 if (!debug.started) {
364 debug.started = true;
365 debug.start = std::chrono::steady_clock::now();
366 }
367#ifndef YAZE_USE_SDL3
368 if (!debug.script.empty() && ImGui::GetCurrentContext() != nullptr) {
369 InputDebugRunScript();
370 }
371#endif
372 }
373
375 while (window_backend_->PollEvent(event)) {
376 // A wake event only ends the idle wait so OnLoad() drains the UI task
377 // queue. It is not user activity: do not reset the idle timer or pass
378 // it to the emulator.
380 continue;
381 }
382#ifndef YAZE_USE_SDL3
383 if (event.has_native_event) {
384 InputDebugLogSdlEvent(event.native_event);
385 }
386#endif
387 last_event_time_ = std::chrono::steady_clock::now();
388 switch (event.type) {
391 // Native close requests must follow the same guarded quit path as the
392 // menu and keyboard shortcut. EditorManager keeps the application
393 // alive while the user resolves any unsaved-session prompt.
395 break;
396
399 window_hidden_ = true;
401 break;
402
405 break;
406
411 window_hidden_ = false;
413 break;
414
415 default:
416 // Other events are handled by ImGui via ProcessNativeEvent
417 // which is called inside PollEvent
418 break;
419 }
420
421 // Forward native SDL events to emulator input for event-based paths
422 if (event.has_native_event) {
423 editor_manager_.emulator().input_manager().ProcessEvent(
424 static_cast<void*>(&event.native_event));
425 }
426 }
427}
428
429absl::StatusOr<std::string> Controller::RequestEditorControl(
430 const std::string& json) {
431 return editor_control_queue_.Submit(json);
432}
433
434absl::Status Controller::OnLoad() {
435 if (!window_backend_) {
436 return absl::InternalError("Window backend not initialized");
437 }
438
439 if (editor_manager_.quit() || !window_backend_->IsActive()) {
440 active_ = false;
441 // No more frames will drain the queue. Cancel waiting service calls now,
442 // before Application::Shutdown() stops the gRPC server, which waits for
443 // in-flight RPCs to return.
445 return absl::OkStatus();
446 }
447
448 // Run work posted by gRPC and HTTP threads before the frame starts, so
449 // the frame draws the state those tasks produced. Tasks must not call
450 // ImGui frame functions: no frame is open yet.
452
453 // Start new ImGui frame via backend (handles SDL2/SDL3 automatically)
454 window_backend_->NewImGuiFrame();
455 ImGui::NewFrame();
456 // Include every application widget, including menus, in this frame.
458 InputDebugLogFrame();
459 editor_control_queue_.ProcessOne([this](const std::string& request) {
461 });
462
463 // Advance any in-progress theme color transitions
465
466 const ImGuiViewport* viewport = ImGui::GetMainViewport();
467
468 // Calculate layout offsets for sidebars and status bar
469 const float left_offset = editor_manager_.GetLeftLayoutOffset();
470 const float right_offset = editor_manager_.GetRightLayoutOffset();
471 float bottom_offset = editor_manager_.GetBottomLayoutOffset();
472
473 float top_offset = 0.0f;
474#if defined(__APPLE__) && \
475 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
476 // On iOS, inset the dockspace by the safe area so content doesn't render
477 // behind the notch/Dynamic Island (top) or home indicator (bottom).
478 {
479 const auto safe = platform::ios::GetSafeAreaInsets();
480 top_offset = std::max(safe.top, platform::ios::GetOverlayTopInset());
481 bottom_offset += safe.bottom;
482 }
483#endif
484
485 // Adjust dockspace position and size for sidebars and status bar
486 ImVec2 dockspace_pos = viewport->WorkPos;
487 ImVec2 dockspace_size = viewport->WorkSize;
488
489 dockspace_pos.x += left_offset;
490 dockspace_pos.y += top_offset;
491 dockspace_size.x -= (left_offset + right_offset);
492 dockspace_size.y -= (bottom_offset + top_offset);
493
494 ImGui::SetNextWindowPos(dockspace_pos);
495 ImGui::SetNextWindowSize(dockspace_size);
496 ImGui::SetNextWindowViewport(viewport->ID);
497
498 // Check if menu bar should be visible (WASM can hide it for clean UI)
499 bool show_menu_bar = true;
502 }
503#if defined(__APPLE__) && \
504 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
505 show_menu_bar = false;
506#endif
507
508 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking;
509 if (show_menu_bar) {
510 window_flags |= ImGuiWindowFlags_MenuBar;
511 }
512 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
513 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
514 window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus |
515 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
516
517 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
518 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
519 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
520 ImGui::Begin("DockSpaceWindow", nullptr, window_flags);
521 ImGui::PopStyleVar(3);
522
523 // Outer dockspace: the editor host fills its central node and pinned
524 // panels share one node beside it. Its ID is a fixed hash (not salted by
525 // the window ID stack) so every caller resolves the same node.
526 const ImGuiID dockspace_id = editor::LayoutManager::OuterDockspaceId();
528 if (auto* mgr = layout_manager) {
529 mgr->SetMainDockspaceId(dockspace_id);
530 // Phase 8.2: one-shot startup reapply of the user's last applied
531 // named layout. Idempotent — fires once per session on the first
532 // frame the dockspace is bound and `last_applied_layout_name` is
533 // set. Errors are logged inside the manager; we deliberately ignore
534 // the return so a stale or removed layout doesn't block the editor
535 // from coming up.
536 (void)mgr->MaybeReapplyStartupLayout(
538 }
539 // NoDockingSplit: panels cannot split the outer tree; they dock inside an
540 // editor's dockspace or in the pin node.
542 dockspace_id, ImVec2(0.0f, 0.0f),
543 ImGuiDockNodeFlags_PassthruCentralNode |
544 ImGuiDockNodeFlags_NoDockingSplit);
545
546 if (show_menu_bar) {
547 editor_manager_.DrawMainMenuBar(); // Draw the fixed menu bar at the top
548 }
549
551
553 bus->Publish(editor::FrameGuiBeginEvent::Create(ImGui::GetIO().DeltaTime));
554 }
555 ImGui::End();
556
557 if (layout_manager) {
558 layout_manager->DrawEditorHost();
559 }
560
561#if !defined(__APPLE__) || \
562 (TARGET_OS_IPHONE != 1 && TARGET_IPHONE_SIMULATOR != 1)
563 // Draw menu bar restore button when menu is hidden (WASM)
564 if (!show_menu_bar && editor_manager_.ui_coordinator()) {
566 }
567#endif
568 absl::Status update_status = editor_manager_.Update();
570 RETURN_IF_ERROR(update_status);
571
572#if defined(__APPLE__) && \
573 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
574 {
576 auto* editor = editor_manager_.GetCurrentEditor();
577 auto* rom = editor_manager_.GetCurrentRom();
578 if (editor) {
579 snap.can_undo = editor->undo_manager().CanUndo();
580 snap.can_redo = editor->undo_manager().CanRedo();
581 snap.editor_type =
583 }
584 if (rom && rom->is_loaded()) {
585 snap.can_save = true;
586 snap.is_dirty = rom->dirty();
587 snap.rom_title = rom->title();
588 }
590 }
591#endif
592
593 return absl::OkStatus();
594}
595
597 if (!window_backend_ || !renderer_)
598 return;
599
600 // Process pending texture commands.
601 // During layout transitions, use a time-budgeted approach to reduce GPU
602 // pressure and avoid Metal crashes from too many texture uploads per frame.
603 // Every 30th frame during transitions, do a full queue pass to prevent starvation.
604 {
605 const auto sync_state = editor_manager_.GetUiSyncStateSnapshot();
606 const bool in_transition = sync_state.layout_rebuild_pending ||
607 sync_state.pending_layout_actions > 0;
608 if (in_transition && (sync_state.frame_id % 30 != 0)) {
610 } else {
612 }
613 }
614
615 if (Application::Instance().GetConfig().headless) {
616 // In HEADLESS mode, we MUST still end the ImGui frame to satisfy assertions
617 // even if we don't render to a window.
618 ImGui::Render();
621 return;
622 }
623
624 renderer_->Clear();
625
626 // Render ImGui draw data and handle viewports via backend
627 // This MUST be called even in headless mode to end the ImGui frame
628 window_backend_->RenderImGui(renderer_.get());
629
630 // Capture the rendered backbuffer before Present invalidates its contents.
632 renderer_->Present();
634
635#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
637#endif
638
639 // Gentle cap for renderers without vsync (software fallback): if the frame
640 // took under 7 ms (> ~144 fps), yield 1 ms. TimingManager is updated once
641 // per frame by EditorManager; updating it here too halved its delta time
642 // and made this check fire almost every frame.
643 const auto frame_end = std::chrono::steady_clock::now();
644 const auto frame_time = frame_end - last_frame_end_;
645 last_frame_end_ = frame_end;
646 if (frame_time < std::chrono::milliseconds(7)) {
647#if TARGET_OS_IPHONE != 1
648 SDL_Delay(1);
649#endif
650 }
651}
652
654#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE && \
655 !defined(YAZE_GUI_TEST_TARGET)
656 // Stop the test engine while its bound ImGui context is still alive.
658#endif
659
660 if (renderer_) {
661 // Retired handles contain no Bitmap pointers and are safe to destroy once
662 // the final submitted frame is no longer using them.
664 gfx::Arena::Get().Initialize(nullptr);
665 renderer_->Shutdown();
666 }
667 // Backstop for exits that skip the quit path in OnLoad(). Clearing the
668 // waker waits for any wake in progress, so no service thread is inside
669 // WakeEventLoop() when the backend shuts down.
670 {
671 auto& ui_queue = util::UiThreadQueue::Get();
672 ui_queue.Shutdown();
673 ui_queue.SetWaker({});
674 }
675 if (window_backend_) {
676 window_backend_->Shutdown();
677 }
678
679#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE && \
680 !defined(YAZE_GUI_TEST_TARGET)
681 // The backend owns and destroys ImGui; release the engine context afterward.
683#endif
684}
685
686absl::Status Controller::LoadRomForTesting(const std::string& rom_path) {
687 // Use EditorManager's OpenRomOrProject which handles the full initialization:
688 // 1. Load ROM file into session
689 // 2. ConfigureEditorDependencies()
690 // 3. LoadAssetsForMode() - initializes all editors and loads graphics
691 // 4. Updates UI state (hides welcome screen, etc.)
692 auto previous_mode = editor_manager_.asset_load_mode();
694 auto status = editor_manager_.OpenRomOrProject(
696 editor_manager_.SetAssetLoadMode(previous_mode);
697 return status;
698}
699
701#ifdef YAZE_WITH_GRPC
702 std::queue<ScreenshotRequest> requests;
703 {
704 std::lock_guard<std::mutex> lock(screenshot_mutex_);
705 requests.swap(screenshot_requests_);
706 }
707 while (!requests.empty()) {
708 auto request = std::move(requests.front());
709 requests.pop();
710
711 absl::StatusOr<test::ScreenshotArtifact> result =
712 absl::FailedPreconditionError(
713 "Screenshots require a rendered GUI frame; use service mode "
714 "instead of headless mode");
715 if (!Application::Instance().GetConfig().headless) {
716 result = request.window_title.empty()
717 ? test::CaptureHarnessScreenshot(request.preferred_path,
718 request.reveal_to_user,
719 request.format)
720 : test::CaptureWindowByName(
721 request.window_title, request.preferred_path,
722 request.reveal_to_user, request.format);
723 }
724 if (request.callback) {
725 request.callback(std::move(result));
726 }
727 }
728#endif
729}
730
731} // namespace yaze
static Application & Instance()
const AppConfig & GetConfig() const
Definition application.h:85
std::chrono::steady_clock::time_point last_frame_end_
Definition controller.h:116
std::queue< ScreenshotRequest > screenshot_requests_
Definition controller.h:124
editor::EditorManager editor_manager_
Definition controller.h:119
absl::Status LoadRomForTesting(const std::string &rom_path)
absl::Status OnEntry(std::string filename="")
absl::StatusOr< std::string > RequestEditorControl(const std::string &request)
app::EditorControlQueue editor_control_queue_
Definition controller.h:126
std::chrono::steady_clock::time_point last_event_time_
Definition controller.h:112
void SetStartupEditor(const std::string &editor_name, const std::string &cards)
std::unique_ptr< platform::IWindowBackend > window_backend_
Definition controller.h:118
void ProcessScreenshotRequests() const
absl::Status OnLoad()
void DoRender() const
std::unique_ptr< gfx::IRenderer > renderer_
Definition controller.h:120
std::mutex screenshot_mutex_
Definition controller.h:123
bool dirty() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:199
auto title() const
Definition rom.h:211
void ProcessOne(const std::function< std::string(const std::string &)> &action)
absl::StatusOr< std::string > Submit(std::string json, std::chrono::milliseconds queue_timeout=std::chrono::seconds(5))
Rom * GetCurrentRom() const override
UICoordinator * ui_coordinator()
void Initialize(gfx::IRenderer *renderer, const std::string &filename="")
auto GetCurrentEditor() const -> Editor *override
absl::Status OpenRomOrProject(const std::string &filename, LoadOptionsPrompt prompt=LoadOptionsPrompt::kAsk)
void HandleHostVisibilityChanged(bool visible)
void SetAssetLoadMode(AssetLoadMode mode)
void OpenEditorAndPanelsFromFlags(const std::string &editor_name, const std::string &panels_str)
absl::Status Update()
Main update loop for the editor application.
std::string ExecuteEditorControl(const std::string &request)
AssetLoadMode asset_load_mode() const
auto emulator() -> emu::Emulator &
UiSyncState GetUiSyncStateSnapshot() const
void DrawEditorHost()
Draw the editor host window and enforce the pin node.
static ImGuiID OuterDockspaceId()
Outer dockspace hosted by DockSpaceWindow.
void Initialize(IRenderer *renderer)
Definition arena.cc:17
bool ProcessTextureQueueWithBudget(IRenderer *renderer, float budget_ms)
Process texture queue with a time budget.
Definition arena.cc:372
size_t DrainRetiredBitmaps(IRenderer *renderer)
Destroy texture handles retired earlier in the frame.
Definition arena.cc:91
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
static Arena & Get()
Definition arena.cc:24
static RendererBackendType GetDefaultBackendType()
static std::unique_ptr< IRenderer > Create(RendererBackendType type=RendererBackendType::kDefault)
static void BeginEnhancedDockSpace(ImGuiID dockspace_id, const ImVec2 &size=ImVec2(0, 0), ImGuiDockNodeFlags flags=0)
static ThemeManager & Get()
static WidgetIdRegistry & Instance()
static WindowBackendType GetDefaultType()
Get the default backend type for this build.
static std::unique_ptr< IWindowBackend > Create(WindowBackendType type)
Create a window backend of the specified type.
static TestManager & Get()
static LogManager & instance()
Definition log.cc:66
void Flush()
Writes buffered file output now. log() flushes warnings and errors at once but batches other lines fo...
Definition log.cc:139
static UiThreadQueue & Get()
void SetWaker(std::function< void()> waker)
#define LOG_INFO(category, format,...)
Definition log.h:136
void InputDebugLogSdlEvent(const SDL_Event &e)
::yaze::EventBus * event_bus()
Get the current EventBus instance.
UserSettings * user_settings()
Get the current UserSettings instance.
LayoutManager * layout_manager()
Get the shared LayoutManager instance.
constexpr std::array< const char *, 15 > kEditorNames
Definition editor.h:232
size_t EditorTypeIndex(EditorType type)
Definition editor.h:242
@ Null
Null renderer for headless/server mode.
@ Metal
Metal renderer backend (Apple platforms)
void PostEditorStateUpdate(const EditorStateSnapshot &state)
SafeAreaInsets GetSafeAreaInsets()
bool IsWakeEvent(const SDL_Event &event)
True for an event pushed by PushWakeEvent().
int MainLoopEventWaitMs(const MainLoopIdleState &state)
Returns how long the frame loop may block waiting for an event.
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::chrono::steady_clock::time_point start
Definition controller.cc:69
static FrameGuiBeginEvent Create(float dt)
Window configuration parameters.
Definition iwindow.h:24
Platform-agnostic window event data.
Definition iwindow.h:65
WindowEventType type
Definition iwindow.h:66
Inputs for the frame loop's idle decision.
std::chrono::steady_clock::duration quiet_for