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 // Initialize the graphics Arena with the renderer
85
86 // Set up audio for emulator (using backend's audio resources)
87 auto audio_buffer = window_backend_->GetAudioBuffer();
88 if (audio_buffer) {
89 editor_manager_.emulator().set_audio_buffer(audio_buffer.get());
90 }
91 editor_manager_.emulator().set_audio_device_id(
92 window_backend_->GetAudioDevice());
93
95 Application::Instance().GetConfig().asset_load_mode);
96
97 // Initialize editor manager with renderer
98 editor_manager_.Initialize(renderer_.get(), filename);
99
100 active_ = true;
101 return absl::OkStatus();
102}
103
104void Controller::SetStartupEditor(const std::string& editor_name,
105 const std::string& panels) {
106 // Process command-line flags for editor and panels
107 // Example: --editor=Dungeon --open_panels="dungeon.room_list,Room 0"
108 if (!editor_name.empty()) {
110 }
111}
112
114 if (!window_backend_)
115 return;
116
118 while (window_backend_->PollEvent(event)) {
119 switch (event.type) {
122 // Native close requests must follow the same guarded quit path as the
123 // menu and keyboard shortcut. EditorManager keeps the application
124 // alive while the user resolves any unsaved-session prompt.
126 break;
127
132 break;
133
139 break;
140
141 default:
142 // Other events are handled by ImGui via ProcessNativeEvent
143 // which is called inside PollEvent
144 break;
145 }
146
147 // Forward native SDL events to emulator input for event-based paths
148 if (event.has_native_event) {
149 editor_manager_.emulator().input_manager().ProcessEvent(
150 static_cast<void*>(&event.native_event));
151 }
152 }
153}
154
155absl::Status Controller::OnLoad() {
156 if (!window_backend_) {
157 return absl::InternalError("Window backend not initialized");
158 }
159
160 if (editor_manager_.quit() || !window_backend_->IsActive()) {
161 active_ = false;
162 return absl::OkStatus();
163 }
164
165 // Start new ImGui frame via backend (handles SDL2/SDL3 automatically)
166 window_backend_->NewImGuiFrame();
167 ImGui::NewFrame();
168
169 // Advance any in-progress theme color transitions
171
172 const ImGuiViewport* viewport = ImGui::GetMainViewport();
173
174 // Calculate layout offsets for sidebars and status bar
175 const float left_offset = editor_manager_.GetLeftLayoutOffset();
176 const float right_offset = editor_manager_.GetRightLayoutOffset();
177 float bottom_offset = editor_manager_.GetBottomLayoutOffset();
178
179 float top_offset = 0.0f;
180#if defined(__APPLE__) && \
181 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
182 // On iOS, inset the dockspace by the safe area so content doesn't render
183 // behind the notch/Dynamic Island (top) or home indicator (bottom).
184 {
185 const auto safe = platform::ios::GetSafeAreaInsets();
186 top_offset = std::max(safe.top, platform::ios::GetOverlayTopInset());
187 bottom_offset += safe.bottom;
188 }
189#endif
190
191 // Adjust dockspace position and size for sidebars and status bar
192 ImVec2 dockspace_pos = viewport->WorkPos;
193 ImVec2 dockspace_size = viewport->WorkSize;
194
195 dockspace_pos.x += left_offset;
196 dockspace_pos.y += top_offset;
197 dockspace_size.x -= (left_offset + right_offset);
198 dockspace_size.y -= (bottom_offset + top_offset);
199
200 ImGui::SetNextWindowPos(dockspace_pos);
201 ImGui::SetNextWindowSize(dockspace_size);
202 ImGui::SetNextWindowViewport(viewport->ID);
203
204 // Check if menu bar should be visible (WASM can hide it for clean UI)
205 bool show_menu_bar = true;
208 }
209#if defined(__APPLE__) && \
210 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
211 show_menu_bar = false;
212#endif
213
214 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking;
215 if (show_menu_bar) {
216 window_flags |= ImGuiWindowFlags_MenuBar;
217 }
218 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
219 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
220 window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus |
221 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
222
223 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
224 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
225 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
226 ImGui::Begin("DockSpaceWindow", nullptr, window_flags);
227 ImGui::PopStyleVar(3);
228
229 // Create DockSpace with adjusted size.
230 // NOTE: ImGui IDs are salted by the current window's ID stack, so this
231 // particular `GetID("MainDockSpace")` is only meaningful while
232 // DockSpaceWindow is the active Begin. Cache it on LayoutManager so
233 // cross-scope callers (e.g. the Layout Designer panel, which lives in
234 // its own PanelWindow) can apply docktrees against the real dockspace
235 // instead of a differently-salted hash.
236 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
238 mgr->SetMainDockspaceId(dockspace_id);
239 // Phase 8.2: one-shot startup reapply of the user's last applied
240 // named layout. Idempotent — fires once per session on the first
241 // frame the dockspace is bound and `last_applied_layout_name` is
242 // set. Errors are logged inside the manager; we deliberately ignore
243 // the return so a stale or removed layout doesn't block the editor
244 // from coming up.
245 (void)mgr->MaybeReapplyStartupLayout(
247 }
249 dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
250
251 if (show_menu_bar) {
252 editor_manager_.DrawMainMenuBar(); // Draw the fixed menu bar at the top
253 }
254
256
258 bus->Publish(editor::FrameGuiBeginEvent::Create(ImGui::GetIO().DeltaTime));
259 }
260 ImGui::End();
261
262#if !defined(__APPLE__) || \
263 (TARGET_OS_IPHONE != 1 && TARGET_IPHONE_SIMULATOR != 1)
264 // Draw menu bar restore button when menu is hidden (WASM)
265 if (!show_menu_bar && editor_manager_.ui_coordinator()) {
267 }
268#endif
270 absl::Status update_status = editor_manager_.Update();
272 RETURN_IF_ERROR(update_status);
273
274#if defined(__APPLE__) && \
275 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
276 {
278 auto* editor = editor_manager_.GetCurrentEditor();
279 auto* rom = editor_manager_.GetCurrentRom();
280 if (editor) {
281 snap.can_undo = editor->undo_manager().CanUndo();
282 snap.can_redo = editor->undo_manager().CanRedo();
283 snap.editor_type =
285 }
286 if (rom && rom->is_loaded()) {
287 snap.can_save = true;
288 snap.is_dirty = rom->dirty();
289 snap.rom_title = rom->title();
290 }
292 }
293#endif
294
295 return absl::OkStatus();
296}
297
299 if (!window_backend_ || !renderer_)
300 return;
301
302 // Process pending texture commands.
303 // During layout transitions, use a time-budgeted approach to reduce GPU
304 // pressure and avoid Metal crashes from too many texture uploads per frame.
305 // Every 30th frame during transitions, do a full queue pass to prevent starvation.
306 {
307 const auto sync_state = editor_manager_.GetUiSyncStateSnapshot();
308 const bool in_transition = sync_state.layout_rebuild_pending ||
309 sync_state.pending_layout_actions > 0;
310 if (in_transition && (sync_state.frame_id % 30 != 0)) {
312 } else {
314 }
315 }
316
317 if (Application::Instance().GetConfig().headless) {
318 // In HEADLESS mode, we MUST still end the ImGui frame to satisfy assertions
319 // even if we don't render to a window.
320 ImGui::Render();
322 return;
323 }
324
325 renderer_->Clear();
326
327 // Render ImGui draw data and handle viewports via backend
328 // This MUST be called even in headless mode to end the ImGui frame
329 window_backend_->RenderImGui(renderer_.get());
330
331 renderer_->Present();
332
333#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
335#endif
336
337 // Process any pending screenshot requests on the main thread after present
339
340 // Get delta time AFTER render for accurate measurement
341 float delta_time = TimingManager::Get().Update();
342
343 // Gentle frame rate cap to prevent excessive CPU usage
344 // Only delay if we're rendering faster than 144 FPS (< 7ms per frame)
345 if (delta_time < 0.007f) {
346#if TARGET_OS_IPHONE != 1
347 SDL_Delay(1); // Tiny delay to yield CPU without affecting ImGui timing
348#endif
349 }
350}
351
353 if (renderer_) {
354 renderer_->Shutdown();
355 }
356 if (window_backend_) {
357 window_backend_->Shutdown();
358 }
359}
360
361absl::Status Controller::LoadRomForTesting(const std::string& rom_path) {
362 // Use EditorManager's OpenRomOrProject which handles the full initialization:
363 // 1. Load ROM file into session
364 // 2. ConfigureEditorDependencies()
365 // 3. LoadAssetsForMode() - initializes all editors and loads graphics
366 // 4. Updates UI state (hides welcome screen, etc.)
367 auto previous_mode = editor_manager_.asset_load_mode();
369 auto status = editor_manager_.OpenRomOrProject(rom_path);
370 editor_manager_.SetAssetLoadMode(previous_mode);
371 return status;
372}
373
375 std::lock_guard<std::mutex> lock(screenshot_mutex_);
376 screenshot_requests_.push(request);
377}
378
380#ifdef YAZE_WITH_GRPC
381 std::lock_guard<std::mutex> lock(screenshot_mutex_);
382 while (!screenshot_requests_.empty()) {
383 auto request = screenshot_requests_.front();
385
386 // Perform capture on main thread
387 auto result = test::CaptureHarnessScreenshot(request.preferred_path);
388 if (request.callback) {
389 request.callback(result);
390 }
391 }
392#endif
393}
394
395} // namespace yaze
static Application & Instance()
const AppConfig & GetConfig() const
Definition application.h:83
std::queue< ScreenshotRequest > screenshot_requests_
Definition controller.h:107
editor::EditorManager editor_manager_
Definition controller.h:102
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:101
void ProcessScreenshotRequests() const
absl::Status OnLoad()
void DoRender() const
void RequestScreenshot(const ScreenshotRequest &request)
std::unique_ptr< gfx::IRenderer > renderer_
Definition controller.h:103
std::mutex screenshot_mutex_
Definition controller.h:106
bool dirty() const
Definition rom.h:145
bool is_loaded() const
Definition rom.h:144
auto title() const
Definition rom.h:149
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:261
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
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