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"
16#include "app/emu/emulator.h"
23#include "app/platform/timing.h"
25#include "imgui/imgui.h"
26#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
28#endif
29#if defined(__APPLE__) && \
30 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
32#endif
33
34namespace yaze {
35
36absl::Status Controller::OnEntry(std::string filename) {
37 // Create window backend using factory (auto-selects SDL2 or SDL3)
40
41 const auto& app_config = Application::Instance().GetConfig();
42
43 if (app_config.headless) {
44 LOG_INFO("Controller", "Using Null Window Backend (Headless Mode)");
46 renderer_type = gfx::RendererBackendType::Null;
47 }
48
49#if defined(__APPLE__) && \
50 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
52 renderer_type = gfx::RendererBackendType::Metal;
53#endif
54
56 if (!window_backend_) {
57 return absl::InternalError("Failed to create window backend");
58 }
59
61 config.title = "Yet Another Zelda3 Editor";
62 config.resizable = true;
63 config.high_dpi =
64 false; // Disabled to match legacy behavior (SDL_WINDOW_RESIZABLE only)
65
66 if (app_config.service_mode) {
67 LOG_INFO("Controller", "Starting in Service Mode (Hidden Window)");
68 config.hidden = true;
69 }
70
71 RETURN_IF_ERROR(window_backend_->Initialize(config));
72
73 // Create renderer via factory (auto-selects SDL2 or SDL3)
75 if (!window_backend_->InitializeRenderer(renderer_.get())) {
76 return absl::InternalError("Failed to initialize renderer");
77 }
78
79 // Initialize ImGui via backend (handles SDL2/SDL3 automatically)
80 RETURN_IF_ERROR(window_backend_->InitializeImGui(renderer_.get()));
81
82 // Initialize the graphics Arena with the renderer
84
85 // Set up audio for emulator (using backend's audio resources)
86 auto audio_buffer = window_backend_->GetAudioBuffer();
87 if (audio_buffer) {
88 editor_manager_.emulator().set_audio_buffer(audio_buffer.get());
89 }
90 editor_manager_.emulator().set_audio_device_id(
91 window_backend_->GetAudioDevice());
92
94 Application::Instance().GetConfig().asset_load_mode);
95
96 // Initialize editor manager with renderer
97 editor_manager_.Initialize(renderer_.get(), filename);
98
99 active_ = true;
100 return absl::OkStatus();
101}
102
103void Controller::SetStartupEditor(const std::string& editor_name,
104 const std::string& panels) {
105 // Process command-line flags for editor and panels
106 // Example: --editor=Dungeon --open_panels="dungeon.room_list,Room 0"
107 if (!editor_name.empty()) {
109 }
110}
111
113 if (!window_backend_)
114 return;
115
117 while (window_backend_->PollEvent(event)) {
118 switch (event.type) {
121 active_ = false;
122 break;
123
128 break;
129
135 break;
136
137 default:
138 // Other events are handled by ImGui via ProcessNativeEvent
139 // which is called inside PollEvent
140 break;
141 }
142
143 // Forward native SDL events to emulator input for event-based paths
144 if (event.has_native_event) {
145 editor_manager_.emulator().input_manager().ProcessEvent(
146 static_cast<void*>(&event.native_event));
147 }
148 }
149}
150
151absl::Status Controller::OnLoad() {
152 if (!window_backend_) {
153 return absl::InternalError("Window backend not initialized");
154 }
155
156 if (editor_manager_.quit() || !window_backend_->IsActive()) {
157 active_ = false;
158 return absl::OkStatus();
159 }
160
161 // Start new ImGui frame via backend (handles SDL2/SDL3 automatically)
162 window_backend_->NewImGuiFrame();
163 ImGui::NewFrame();
164
165 // Advance any in-progress theme color transitions
167
168 const ImGuiViewport* viewport = ImGui::GetMainViewport();
169
170 // Calculate layout offsets for sidebars and status bar
171 const float left_offset = editor_manager_.GetLeftLayoutOffset();
172 const float right_offset = editor_manager_.GetRightLayoutOffset();
173 float bottom_offset = editor_manager_.GetBottomLayoutOffset();
174
175 float top_offset = 0.0f;
176#if defined(__APPLE__) && \
177 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
178 // On iOS, inset the dockspace by the safe area so content doesn't render
179 // behind the notch/Dynamic Island (top) or home indicator (bottom).
180 {
181 const auto safe = platform::ios::GetSafeAreaInsets();
182 top_offset = std::max(safe.top, platform::ios::GetOverlayTopInset());
183 bottom_offset += safe.bottom;
184 }
185#endif
186
187 // Adjust dockspace position and size for sidebars and status bar
188 ImVec2 dockspace_pos = viewport->WorkPos;
189 ImVec2 dockspace_size = viewport->WorkSize;
190
191 dockspace_pos.x += left_offset;
192 dockspace_pos.y += top_offset;
193 dockspace_size.x -= (left_offset + right_offset);
194 dockspace_size.y -= (bottom_offset + top_offset);
195
196 ImGui::SetNextWindowPos(dockspace_pos);
197 ImGui::SetNextWindowSize(dockspace_size);
198 ImGui::SetNextWindowViewport(viewport->ID);
199
200 // Check if menu bar should be visible (WASM can hide it for clean UI)
201 bool show_menu_bar = true;
204 }
205#if defined(__APPLE__) && \
206 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
207 show_menu_bar = false;
208#endif
209
210 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking;
211 if (show_menu_bar) {
212 window_flags |= ImGuiWindowFlags_MenuBar;
213 }
214 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
215 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
216 window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus |
217 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
218
219 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
220 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
221 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
222 ImGui::Begin("DockSpaceWindow", nullptr, window_flags);
223 ImGui::PopStyleVar(3);
224
225 // Create DockSpace with adjusted size
226 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
228 dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
229
230 if (show_menu_bar) {
231 editor_manager_.DrawMainMenuBar(); // Draw the fixed menu bar at the top
232 }
233
235
237 bus->Publish(editor::FrameGuiBeginEvent::Create(ImGui::GetIO().DeltaTime));
238 }
239 ImGui::End();
240
241#if !defined(__APPLE__) || \
242 (TARGET_OS_IPHONE != 1 && TARGET_IPHONE_SIMULATOR != 1)
243 // Draw menu bar restore button when menu is hidden (WASM)
244 if (!show_menu_bar && editor_manager_.ui_coordinator()) {
246 }
247#endif
249 absl::Status update_status = editor_manager_.Update();
251 RETURN_IF_ERROR(update_status);
252
253#if defined(__APPLE__) && \
254 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
255 {
257 auto* editor = editor_manager_.GetCurrentEditor();
258 auto* rom = editor_manager_.GetCurrentRom();
259 if (editor) {
260 snap.can_undo = editor->undo_manager().CanUndo();
261 snap.can_redo = editor->undo_manager().CanRedo();
262 snap.editor_type =
264 }
265 if (rom && rom->is_loaded()) {
266 snap.can_save = true;
267 snap.is_dirty = rom->dirty();
268 snap.rom_title = rom->title();
269 }
271 }
272#endif
273
274 return absl::OkStatus();
275}
276
278 if (!window_backend_ || !renderer_)
279 return;
280
281 // Process pending texture commands.
282 // During layout transitions, use a time-budgeted approach to reduce GPU
283 // pressure and avoid Metal crashes from too many texture uploads per frame.
284 // Every 30th frame during transitions, do a full queue pass to prevent starvation.
285 {
286 const auto sync_state = editor_manager_.GetUiSyncStateSnapshot();
287 const bool in_transition = sync_state.layout_rebuild_pending ||
288 sync_state.pending_layout_actions > 0;
289 if (in_transition && (sync_state.frame_id % 30 != 0)) {
291 } else {
293 }
294 }
295
296 if (Application::Instance().GetConfig().headless) {
297 // In HEADLESS mode, we MUST still end the ImGui frame to satisfy assertions
298 // even if we don't render to a window.
299 ImGui::Render();
301 return;
302 }
303
304 renderer_->Clear();
305
306 // Render ImGui draw data and handle viewports via backend
307 // This MUST be called even in headless mode to end the ImGui frame
308 window_backend_->RenderImGui(renderer_.get());
309
310 renderer_->Present();
311
312#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
314#endif
315
316 // Process any pending screenshot requests on the main thread after present
318
319 // Get delta time AFTER render for accurate measurement
320 float delta_time = TimingManager::Get().Update();
321
322 // Gentle frame rate cap to prevent excessive CPU usage
323 // Only delay if we're rendering faster than 144 FPS (< 7ms per frame)
324 if (delta_time < 0.007f) {
325#if TARGET_OS_IPHONE != 1
326 SDL_Delay(1); // Tiny delay to yield CPU without affecting ImGui timing
327#endif
328 }
329}
330
332 if (renderer_) {
333 renderer_->Shutdown();
334 }
335 if (window_backend_) {
336 window_backend_->Shutdown();
337 }
338}
339
340absl::Status Controller::LoadRomForTesting(const std::string& rom_path) {
341 // Use EditorManager's OpenRomOrProject which handles the full initialization:
342 // 1. Load ROM file into session
343 // 2. ConfigureEditorDependencies()
344 // 3. LoadAssetsForMode() - initializes all editors and loads graphics
345 // 4. Updates UI state (hides welcome screen, etc.)
346 auto previous_mode = editor_manager_.asset_load_mode();
348 auto status = editor_manager_.OpenRomOrProject(rom_path);
349 editor_manager_.SetAssetLoadMode(previous_mode);
350 return status;
351}
352
354 std::lock_guard<std::mutex> lock(screenshot_mutex_);
355 screenshot_requests_.push(request);
356}
357
359#ifdef YAZE_WITH_GRPC
360 std::lock_guard<std::mutex> lock(screenshot_mutex_);
361 while (!screenshot_requests_.empty()) {
362 auto request = screenshot_requests_.front();
364
365 // Perform capture on main thread
366 auto result = test::CaptureHarnessScreenshot(request.preferred_path);
367 if (request.callback) {
368 request.callback(result);
369 }
370 }
371#endif
372}
373
374} // namespace yaze
static Application & Instance()
const AppConfig & GetConfig() const
Definition application.h:83
std::queue< ScreenshotRequest > screenshot_requests_
Definition controller.h:94
editor::EditorManager editor_manager_
Definition controller.h:89
absl::Status LoadRomForTesting(const std::string &rom_path)
absl::Status OnEntry(std::string filename="")
Definition controller.cc:36
void SetStartupEditor(const std::string &editor_name, const std::string &cards)
std::unique_ptr< platform::IWindowBackend > window_backend_
Definition controller.h:88
void ProcessScreenshotRequests() const
absl::Status OnLoad()
void DoRender() const
void RequestScreenshot(const ScreenshotRequest &request)
std::unique_ptr< gfx::IRenderer > renderer_
Definition controller.h:90
std::mutex screenshot_mutex_
Definition controller.h:93
bool dirty() const
Definition rom.h:133
bool is_loaded() const
Definition rom.h:132
auto title() const
Definition rom.h:137
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="")
void HandleHostVisibilityChanged(bool visible)
auto GetCurrentEditor() const -> Editor *
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.
constexpr std::array< const char *, 14 > kEditorNames
Definition editor.h:217
size_t EditorTypeIndex(EditorType type)
Definition editor.h:226
@ 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