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 <string>
11
12#include "absl/status/status.h"
17#include "app/emu/emulator.h"
24#include "app/platform/timing.h"
26#include "imgui/imgui.h"
27#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
29#endif
30#if defined(__APPLE__) && \
31 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
33#endif
34
35namespace yaze {
36
37absl::Status Controller::OnEntry(std::string filename) {
38 // Create window backend using factory (auto-selects SDL2 or SDL3)
41
42 const auto& app_config = Application::Instance().GetConfig();
43
44 if (app_config.headless) {
45 LOG_INFO("Controller", "Using Null Window Backend (Headless Mode)");
47 renderer_type = gfx::RendererBackendType::Null;
48 }
49
50#if defined(__APPLE__) && \
51 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
53 renderer_type = gfx::RendererBackendType::Metal;
54#endif
55
57 if (!window_backend_) {
58 return absl::InternalError("Failed to create window backend");
59 }
60
62 config.title = "Yet Another Zelda3 Editor";
63 config.resizable = true;
64 config.high_dpi =
65 false; // Disabled to match legacy behavior (SDL_WINDOW_RESIZABLE only)
66
67 if (app_config.service_mode) {
68 LOG_INFO("Controller", "Starting in Service Mode (Hidden Window)");
69 config.hidden = true;
70 }
71
72 RETURN_IF_ERROR(window_backend_->Initialize(config));
73
74 // Create renderer via factory (auto-selects SDL2 or SDL3)
76 if (!window_backend_->InitializeRenderer(renderer_.get())) {
77 return absl::InternalError("Failed to initialize renderer");
78 }
79
80 // Initialize ImGui via backend (handles SDL2/SDL3 automatically)
81 RETURN_IF_ERROR(window_backend_->InitializeImGui(renderer_.get()));
82
83 // The test manager is linked at the controller/executable layer rather than
84 // the platform window library. Initialize its engine here, after ImGui has a
85 // context, so remote harness actions use the real engine. The dedicated GUI
86 // test runner owns a separate engine and initializes it below its OnEntry().
87#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE && \
88 !defined(YAZE_GUI_TEST_TARGET)
90#endif
91
92 // Initialize the graphics Arena with the renderer
94
95 // Set up audio for emulator (using backend's audio resources)
96 auto audio_buffer = window_backend_->GetAudioBuffer();
97 if (audio_buffer) {
98 editor_manager_.emulator().set_audio_buffer(audio_buffer.get());
99 }
100 editor_manager_.emulator().set_audio_device_id(
101 window_backend_->GetAudioDevice());
102
104 Application::Instance().GetConfig().asset_load_mode);
105
106 // Initialize editor manager with renderer
107 editor_manager_.Initialize(renderer_.get(), filename);
108
109 active_ = true;
110 return absl::OkStatus();
111}
112
113void Controller::SetStartupEditor(const std::string& editor_name,
114 const std::string& panels) {
115 // Process command-line flags for editor and panels
116 // Example: --editor=Dungeon --open_panels="dungeon.room_list,Room 0"
117 if (!editor_name.empty()) {
119 }
120}
121
123 if (!window_backend_)
124 return;
125
127 while (window_backend_->PollEvent(event)) {
128 switch (event.type) {
131 // Native close requests must follow the same guarded quit path as the
132 // menu and keyboard shortcut. EditorManager keeps the application
133 // alive while the user resolves any unsaved-session prompt.
135 break;
136
141 break;
142
148 break;
149
150 default:
151 // Other events are handled by ImGui via ProcessNativeEvent
152 // which is called inside PollEvent
153 break;
154 }
155
156 // Forward native SDL events to emulator input for event-based paths
157 if (event.has_native_event) {
158 editor_manager_.emulator().input_manager().ProcessEvent(
159 static_cast<void*>(&event.native_event));
160 }
161 }
162}
163
164absl::Status Controller::OnLoad() {
165 if (!window_backend_) {
166 return absl::InternalError("Window backend not initialized");
167 }
168
169 if (editor_manager_.quit() || !window_backend_->IsActive()) {
170 active_ = false;
171 return absl::OkStatus();
172 }
173
174 // Start new ImGui frame via backend (handles SDL2/SDL3 automatically)
175 window_backend_->NewImGuiFrame();
176 ImGui::NewFrame();
177
178 // Advance any in-progress theme color transitions
180
181 const ImGuiViewport* viewport = ImGui::GetMainViewport();
182
183 // Calculate layout offsets for sidebars and status bar
184 const float left_offset = editor_manager_.GetLeftLayoutOffset();
185 const float right_offset = editor_manager_.GetRightLayoutOffset();
186 float bottom_offset = editor_manager_.GetBottomLayoutOffset();
187
188 float top_offset = 0.0f;
189#if defined(__APPLE__) && \
190 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
191 // On iOS, inset the dockspace by the safe area so content doesn't render
192 // behind the notch/Dynamic Island (top) or home indicator (bottom).
193 {
194 const auto safe = platform::ios::GetSafeAreaInsets();
195 top_offset = std::max(safe.top, platform::ios::GetOverlayTopInset());
196 bottom_offset += safe.bottom;
197 }
198#endif
199
200 // Adjust dockspace position and size for sidebars and status bar
201 ImVec2 dockspace_pos = viewport->WorkPos;
202 ImVec2 dockspace_size = viewport->WorkSize;
203
204 dockspace_pos.x += left_offset;
205 dockspace_pos.y += top_offset;
206 dockspace_size.x -= (left_offset + right_offset);
207 dockspace_size.y -= (bottom_offset + top_offset);
208
209 ImGui::SetNextWindowPos(dockspace_pos);
210 ImGui::SetNextWindowSize(dockspace_size);
211 ImGui::SetNextWindowViewport(viewport->ID);
212
213 // Check if menu bar should be visible (WASM can hide it for clean UI)
214 bool show_menu_bar = true;
217 }
218#if defined(__APPLE__) && \
219 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
220 show_menu_bar = false;
221#endif
222
223 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking;
224 if (show_menu_bar) {
225 window_flags |= ImGuiWindowFlags_MenuBar;
226 }
227 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
228 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
229 window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus |
230 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
231
232 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
233 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
234 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
235 ImGui::Begin("DockSpaceWindow", nullptr, window_flags);
236 ImGui::PopStyleVar(3);
237
238 // Create DockSpace with adjusted size.
239 // NOTE: ImGui IDs are salted by the current window's ID stack, so this
240 // particular `GetID("MainDockSpace")` is only meaningful while
241 // DockSpaceWindow is the active Begin. Cache it on LayoutManager so
242 // cross-scope callers (e.g. the Layout Designer panel, which lives in
243 // its own PanelWindow) can apply docktrees against the real dockspace
244 // instead of a differently-salted hash.
245 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
247 mgr->SetMainDockspaceId(dockspace_id);
248 // Phase 8.2: one-shot startup reapply of the user's last applied
249 // named layout. Idempotent — fires once per session on the first
250 // frame the dockspace is bound and `last_applied_layout_name` is
251 // set. Errors are logged inside the manager; we deliberately ignore
252 // the return so a stale or removed layout doesn't block the editor
253 // from coming up.
254 (void)mgr->MaybeReapplyStartupLayout(
256 }
258 dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
259
260 if (show_menu_bar) {
261 editor_manager_.DrawMainMenuBar(); // Draw the fixed menu bar at the top
262 }
263
265
267 bus->Publish(editor::FrameGuiBeginEvent::Create(ImGui::GetIO().DeltaTime));
268 }
269 ImGui::End();
270
271#if !defined(__APPLE__) || \
272 (TARGET_OS_IPHONE != 1 && TARGET_IPHONE_SIMULATOR != 1)
273 // Draw menu bar restore button when menu is hidden (WASM)
274 if (!show_menu_bar && editor_manager_.ui_coordinator()) {
276 }
277#endif
279 absl::Status update_status = editor_manager_.Update();
281 RETURN_IF_ERROR(update_status);
282
283#if defined(__APPLE__) && \
284 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
285 {
287 auto* editor = editor_manager_.GetCurrentEditor();
288 auto* rom = editor_manager_.GetCurrentRom();
289 if (editor) {
290 snap.can_undo = editor->undo_manager().CanUndo();
291 snap.can_redo = editor->undo_manager().CanRedo();
292 snap.editor_type =
294 }
295 if (rom && rom->is_loaded()) {
296 snap.can_save = true;
297 snap.is_dirty = rom->dirty();
298 snap.rom_title = rom->title();
299 }
301 }
302#endif
303
304 return absl::OkStatus();
305}
306
308 if (!window_backend_ || !renderer_)
309 return;
310
311 // Process pending texture commands.
312 // During layout transitions, use a time-budgeted approach to reduce GPU
313 // pressure and avoid Metal crashes from too many texture uploads per frame.
314 // Every 30th frame during transitions, do a full queue pass to prevent starvation.
315 {
316 const auto sync_state = editor_manager_.GetUiSyncStateSnapshot();
317 const bool in_transition = sync_state.layout_rebuild_pending ||
318 sync_state.pending_layout_actions > 0;
319 if (in_transition && (sync_state.frame_id % 30 != 0)) {
321 } else {
323 }
324 }
325
326 if (Application::Instance().GetConfig().headless) {
327 // In HEADLESS mode, we MUST still end the ImGui frame to satisfy assertions
328 // even if we don't render to a window.
329 ImGui::Render();
331 return;
332 }
333
334 renderer_->Clear();
335
336 // Render ImGui draw data and handle viewports via backend
337 // This MUST be called even in headless mode to end the ImGui frame
338 window_backend_->RenderImGui(renderer_.get());
339
340 renderer_->Present();
341
342#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
344#endif
345
346 // Process any pending screenshot requests on the main thread after present
348
349 // Get delta time AFTER render for accurate measurement
350 float delta_time = TimingManager::Get().Update();
351
352 // Gentle frame rate cap to prevent excessive CPU usage
353 // Only delay if we're rendering faster than 144 FPS (< 7ms per frame)
354 if (delta_time < 0.007f) {
355#if TARGET_OS_IPHONE != 1
356 SDL_Delay(1); // Tiny delay to yield CPU without affecting ImGui timing
357#endif
358 }
359}
360
362#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE && \
363 !defined(YAZE_GUI_TEST_TARGET)
364 // Stop the test engine while its bound ImGui context is still alive.
366#endif
367
368 if (renderer_) {
369 renderer_->Shutdown();
370 }
371 if (window_backend_) {
372 window_backend_->Shutdown();
373 }
374
375#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE && \
376 !defined(YAZE_GUI_TEST_TARGET)
377 // The backend owns and destroys ImGui; release the engine context afterward.
379#endif
380}
381
382absl::Status Controller::LoadRomForTesting(const std::string& rom_path) {
383 // Use EditorManager's OpenRomOrProject which handles the full initialization:
384 // 1. Load ROM file into session
385 // 2. ConfigureEditorDependencies()
386 // 3. LoadAssetsForMode() - initializes all editors and loads graphics
387 // 4. Updates UI state (hides welcome screen, etc.)
388 auto previous_mode = editor_manager_.asset_load_mode();
390 auto status = editor_manager_.OpenRomOrProject(rom_path);
391 editor_manager_.SetAssetLoadMode(previous_mode);
392 return status;
393}
394
396 std::lock_guard<std::mutex> lock(screenshot_mutex_);
397 screenshot_requests_.push(request);
398}
399
401#ifdef YAZE_WITH_GRPC
402 std::lock_guard<std::mutex> lock(screenshot_mutex_);
403 while (!screenshot_requests_.empty()) {
404 auto request = screenshot_requests_.front();
406
407 // Perform capture on main thread
408 auto result = test::CaptureHarnessScreenshot(request.preferred_path,
409 request.reveal_to_user);
410 if (request.callback) {
411 request.callback(result);
412 }
413 }
414#endif
415}
416
417} // namespace yaze
static Application & Instance()
const AppConfig & GetConfig() const
Definition application.h:83
std::queue< ScreenshotRequest > screenshot_requests_
Definition controller.h:108
editor::EditorManager editor_manager_
Definition controller.h:103
absl::Status LoadRomForTesting(const std::string &rom_path)
absl::Status OnEntry(std::string filename="")
Definition controller.cc:37
void SetStartupEditor(const std::string &editor_name, const std::string &cards)
std::unique_ptr< platform::IWindowBackend > window_backend_
Definition controller.h:102
void ProcessScreenshotRequests() const
absl::Status OnLoad()
void DoRender() const
void RequestScreenshot(const ScreenshotRequest &request)
std::unique_ptr< gfx::IRenderer > renderer_
Definition controller.h:104
std::mutex screenshot_mutex_
Definition controller.h:107
bool dirty() const
Definition rom.h:156
bool is_loaded() const
Definition rom.h:155
auto title() const
Definition rom.h:167
static TimingManager & Get()
Definition timing.h:20
float Update()
Update the timing manager (call once per frame)
Definition timing.h:29
Rom * GetCurrentRom() const override
UICoordinator * ui_coordinator()
void Initialize(gfx::IRenderer *renderer, const std::string &filename="")
auto GetCurrentEditor() const -> Editor *override
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.
AssetLoadMode asset_load_mode() const
auto emulator() -> emu::Emulator &
absl::Status OpenRomOrProject(const std::string &filename)
UiSyncState GetUiSyncStateSnapshot() const
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:312
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:145
static Arena & Get()
Definition arena.cc:21
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()
#define LOG_INFO(category, format,...)
Definition log.h:105
::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 *, 14 > kEditorNames
Definition editor.h:226
size_t EditorTypeIndex(EditorType type)
Definition editor.h:235
@ Null
Null renderer for headless/server mode.
@ Metal
Metal renderer backend (Apple platforms)
void PostEditorStateUpdate(const EditorStateSnapshot &state)
SafeAreaInsets GetSafeAreaInsets()
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
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