yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
application.cc
Go to the documentation of this file.
1#include "app/application.h"
2
3#include <atomic>
4#include <chrono>
5#include <cstdlib>
6#include <ctime>
7#include <filesystem>
8#include <memory>
9#include <string>
10#include <utility>
11#include "absl/cleanup/cleanup.h"
12#include "absl/status/status.h"
13#include "activity_file.h"
14#include "controller.h"
15#include "emu/emulator.h"
16#include "emu/i_emulator.h"
17
18#ifndef _WIN32
19#include <unistd.h> // getpid()
20#endif
21
22#include "absl/strings/str_cat.h"
23#include "absl/strings/str_format.h"
26#include "util/log.h"
27
28#ifdef YAZE_WITH_GRPC
35#endif
36
37#ifdef __EMSCRIPTEN__
38#include <emscripten.h>
41#endif
42
43namespace yaze {
44
46 static Application instance;
47 return instance;
48}
49
51 config_ = config;
52 LOG_INFO("App", "Initializing Application instance...");
53
54 controller_ = std::make_unique<Controller>();
55 if (controller_->editor_manager()) {
56 controller_->editor_manager()->SetStartupLoadHints(config_);
57 }
58
59 // Process pending ROM load if we have one (from flags/config - non-WASM only)
60 std::string start_path = config_.rom_file;
61
62#ifndef __EMSCRIPTEN__
63 if (!pending_rom_.empty()) {
64 // Pending ROM takes precedence over config (e.g. drag-drop before init)
65 start_path = pending_rom_;
66 pending_rom_.clear();
67 LOG_INFO("App", "Found pending ROM load: %s", start_path.c_str());
68 } else if (!start_path.empty()) {
69 LOG_INFO("App", "Using configured startup ROM: %s", start_path.c_str());
70 } else {
71 LOG_INFO("App", "No pending ROM, starting empty.");
72 }
73#else
74 LOG_INFO("App", "WASM build - ROM loading handled via wasm_bootstrap queue.");
75 // In WASM, start_path from config might be ignored if we rely on web uploads
76 // But we can still try to pass it if it's a server-hosted ROM
77#endif
78
79 // Always call OnEntry to initialize Window/Renderer, even with empty path
80 auto status = controller_->OnEntry(start_path);
81 if (!status.ok()) {
82 LOG_ERROR("App", "Failed to initialize controller: %s",
83 std::string(status.message()).c_str());
84 // Window/renderer/ImGui init failed. Do not fall through into the frame
85 // loop with uninitialized state: the controller stays inactive, so the
86 // main loop exits immediately and Shutdown() tears down anything created.
87 return;
88 } else {
89 LOG_INFO("App", "Controller initialized successfully. Active: %s",
90 controller_->IsActive() ? "Yes" : "No");
91
92#ifdef YAZE_WITH_GRPC
93 test::TestManager::Get().SetFailureScreenshotRequester(
94 [controller = controller_.get()](
95 const std::string& preferred_path,
96 test::TestManager::FailureScreenshotCallback callback) {
97 controller->RequestScreenshot({.preferred_path = preferred_path,
98 .reveal_to_user = false,
99 .callback = std::move(callback)});
100 });
101#endif
102
103 if (controller_->editor_manager()) {
104 // Settings > Test mode stands in for the startup flags, because a GUI
105 // agent that launches the app by double-click cannot pass any.
106 // Explicit flags still win.
107 if (controller_->editor_manager()->user_settings().prefs().test_mode) {
108 LOG_INFO("App", "Test mode: hiding welcome and editor picker");
111 }
114 }
116 // Test mode is for local agents only; do not expose ROM writes to
117 // the network.
119 }
120 controller_->editor_manager()->ApplyStartupVisibility(config_);
121 }
122
123 // If we successfully loaded a ROM at startup, run startup actions
124 if (!start_path.empty() && controller_->editor_manager()) {
126 }
127
128#ifdef YAZE_WITH_GRPC
129 // Initialize gRPC unified server
131 LOG_INFO("App", "Initializing Unified gRPC Server...");
132 canvas_automation_service_ =
133 std::make_unique<CanvasAutomationServiceImpl>();
134 grpc_server_ = std::make_unique<YazeGRPCServer>();
135 grpc_server_->SetBindAddress(config_.test_harness_bind_address);
136
137 auto rom_getter = [this]() {
138 return controller_->GetCurrentRom();
139 };
140 auto rom_loader = [this](const std::string& path) -> bool {
141 if (!controller_ || !controller_->editor_manager())
142 return false;
143 auto status = controller_->editor_manager()->OpenRomOrProject(path);
144 return status.ok();
145 };
146
147 emu::IEmulator* emulator_interface = nullptr;
148
149 if (config_.backend == "mesen") {
150 LOG_INFO("App", "Using Mesen2 backend for emulator service");
151 emulator_backend_ =
152 std::make_unique<emu::mesen::MesenEmulatorAdapter>();
153 emulator_interface = emulator_backend_.get();
154 } else {
155 emu::Emulator* internal_emulator = nullptr;
156 if (controller_->editor_manager()) {
157 internal_emulator = &controller_->editor_manager()->emulator();
158 } else {
159 LOG_WARN("App",
160 "EditorManager not ready; internal emulator services may be "
161 "limited");
162 }
163
164 auto adapter =
165 std::make_unique<emu::InternalEmulatorAdapter>(internal_emulator);
166
167 // Set up internal helpers for the adapter
168 adapter->SetRomLoader([this](const std::string& path) -> bool {
169 if (!controller_ || !controller_->editor_manager())
170 return false;
171 auto status = controller_->editor_manager()->OpenRomOrProject(path);
172 return status.ok();
173 });
174
175 adapter->SetRomGetter(
176 [this]() { return controller_->GetCurrentRom(); });
177
178 emulator_backend_ = std::move(adapter);
179 emulator_interface = emulator_backend_.get();
180 }
181
182 // Initialize server with all services
183 auto status = grpc_server_->Initialize(
184 config_.test_harness_port, emulator_interface, rom_getter, rom_loader,
186 nullptr, // Version manager not ready
187 nullptr, // Approval manager not ready
188 canvas_automation_service_.get());
189
190 // GetGameState runs on gRPC threads; capture on the render thread.
191 // Write a PNG to a unique temp path; GetGameState reads it into the
192 // reply and deletes it.
193 grpc_server_->SetEmulatorScreenshotCapturer([] {
194 static std::atomic<int> next_capture{0};
195 const auto path = std::filesystem::temp_directory_path() /
196 absl::StrFormat("yaze_gamestate_%d_%d.png",
197 static_cast<int>(getpid()),
198 next_capture.fetch_add(1));
199 return test::CaptureScreenshotOnRenderThread(
200 path.string(), /*window_title=*/"", test::ScreenshotFormat::kPng);
201 });
202
203 if (status.ok()) {
204 status = grpc_server_->StartAsync(); // Start in background thread
205 if (!status.ok()) {
206 LOG_ERROR("App", "Failed to start gRPC server: %s",
207 std::string(status.message()).c_str());
208 } else {
209 LOG_INFO("App", "Unified gRPC server started on port %d",
211 }
212 } else {
213 LOG_ERROR("App", "Failed to initialize gRPC server: %s",
214 std::string(status.message()).c_str());
215 }
216
217 // Connect services to controller/editor manager
218 if (canvas_automation_service_) {
219 controller_->SetCanvasAutomationService(
220 canvas_automation_service_.get());
221 }
222 }
223#endif
224 }
225
226#ifdef __EMSCRIPTEN__
227 // Register the ROM load handler now that controller is ready.
228 yaze::app::wasm::SetRomLoadHandler(
229 [](std::string path) { Application::Instance().LoadRom(path); });
230#else
231 // Create activity file for instance discovery (non-WASM only).
232 // Prefer $XDG_RUNTIME_DIR (0700 per-user) over world-readable /tmp.
233 auto pid = getpid();
234 const char* runtime_dir = std::getenv("XDG_RUNTIME_DIR");
235 const char* status_dir = (runtime_dir && *runtime_dir) ? runtime_dir : "/tmp";
236 activity_file_ = std::make_unique<app::ActivityFile>(
237 absl::StrFormat("%s/yaze-%d.status", status_dir, pid));
238 UpdateActivityStatus();
239 LOG_INFO("App", "Activity file created: %s",
240 activity_file_->GetPath().c_str());
241#endif
242}
243
244void Application::Tick() {
245 if (!controller_)
246 return;
247 // A blocking native dialog (the iOS document picker) runs a nested run loop
248 // from inside a frame, and the display link then calls Tick() again.
249 // Starting an ImGui frame inside another one aborts in NewFrame().
250 if (in_tick_)
251 return;
252 in_tick_ = true;
253 absl::Cleanup end_tick = [this] {
254 in_tick_ = false;
255 };
256
257 // Calculate delta time
258 auto now = std::chrono::steady_clock::now();
259 if (first_frame_) {
260 delta_time_ = 0.016f; // Assume ~60fps for first frame
261 first_frame_ = false;
262 } else {
263 auto elapsed = std::chrono::duration<float>(now - last_frame_time_);
264 delta_time_ = elapsed.count();
265 }
266 last_frame_time_ = now;
267
268 // Publish FrameBeginEvent for pre-frame (non-ImGui) work
269 if (auto* bus = editor::ContentRegistry::Context::event_bus()) {
270 bus->Publish(editor::FrameBeginEvent::Create(delta_time_));
271 }
272
273#ifdef __EMSCRIPTEN__
274 auto& wasm_collab = app::platform::GetWasmCollaborationInstance();
275 wasm_collab.ProcessPendingChanges();
276#endif
277
278 controller_->OnInput();
279 auto status = controller_->OnLoad();
280 if (!status.ok()) {
281 LOG_ERROR("App", "Controller Load Error: %s",
282 std::string(status.message()).c_str());
283#ifdef __EMSCRIPTEN__
284 emscripten_cancel_main_loop();
285#endif
286 return;
287 }
288
289 if (!controller_->IsActive()) {
290 // Window closed
291 // LOG_INFO("App", "Controller became inactive");
292 }
293
294 controller_->DoRender();
295
296 // Publish FrameEndEvent for cleanup operations
297 if (auto* bus = editor::ContentRegistry::Context::event_bus()) {
298 bus->Publish(editor::FrameEndEvent::Create(delta_time_));
299 }
300}
301
302void Application::LoadRom(const std::string& path) {
303 LOG_INFO("App", "Requesting ROM load: %s", path.c_str());
304
305 if (!controller_) {
306#ifdef __EMSCRIPTEN__
307 yaze::app::wasm::TriggerRomLoad(path);
308 LOG_INFO("App",
309 "Forwarded to wasm_bootstrap queue (controller not ready): %s",
310 path.c_str());
311#else
312 pending_rom_ = path;
313 LOG_INFO("App", "Queued ROM load (controller not ready): %s", path.c_str());
314#endif
315 return;
316 }
317
318 // Controller exists.
319 absl::Status status;
320 if (!controller_->IsActive()) {
321 status = controller_->OnEntry(path);
322 } else {
323 status = controller_->editor_manager()->OpenRomOrProject(path);
324 }
325
326 if (!status.ok()) {
327 std::string error_msg =
328 absl::StrCat("Failed to load ROM: ", status.message());
329 LOG_ERROR("App", "%s", error_msg.c_str());
330
331#ifdef __EMSCRIPTEN__
332 EM_ASM(
333 {
334 var msg = UTF8ToString($0);
335 console.error(msg);
336 alert(msg);
337 },
338 error_msg.c_str());
339#endif
340 } else {
341 LOG_INFO("App", "ROM loaded successfully: %s", path.c_str());
342
343 // Run startup actions whenever a new ROM is loaded IF it matches our startup config
344 // (Optional: we might only want to run actions once at startup, but for CLI usage usually
345 // you load one ROM and want the actions applied to it).
346 // For now, we'll only run actions if this is the first load or if explicitly requested.
347 // Actually, simpler: just run them. The user can close cards if they want.
348 RunStartupActions();
349
350#ifndef __EMSCRIPTEN__
351 // Update activity file with new ROM path
352 UpdateActivityStatus();
353#endif
354
355#ifdef __EMSCRIPTEN__
356 EM_ASM(
357 { console.log("ROM loaded successfully: " + UTF8ToString($0)); },
358 path.c_str());
359#endif
360 }
361}
362
363void Application::RunStartupActions() {
364 if (!controller_ || !controller_->editor_manager())
365 return;
366
367 auto* manager = controller_->editor_manager();
368 manager->ProcessStartupActions(config_);
369}
370
371#ifndef __EMSCRIPTEN__
372void Application::UpdateActivityStatus() {
373 if (!activity_file_)
374 return;
375
376 app::ActivityStatus status;
377 status.pid = getpid();
379 status.start_timestamp = std::time(nullptr);
380
381 if (controller_ && controller_->editor_manager()) {
382 auto* rom = controller_->GetCurrentRom();
383 status.active_rom = rom ? rom->filename() : "";
384 }
385
386#ifdef YAZE_WITH_GRPC
387 if (config_.enable_test_harness && config_.test_harness_port > 0) {
388 status.socket_path =
389 absl::StrFormat("localhost:%d", config_.test_harness_port);
390 }
391#endif
392
393 activity_file_->Update(status);
394}
395#endif
396
397#ifdef __EMSCRIPTEN__
398extern "C" void SyncFilesystem();
399#endif
400
401void Application::Shutdown() {
402#ifdef __EMSCRIPTEN__
403 // Sync IDBFS to persist any changes before shutdown
404 LOG_INFO("App", "Syncing filesystem before shutdown...");
405 SyncFilesystem();
406#endif
407
408#ifdef YAZE_WITH_GRPC
409 if (grpc_server_) {
410 LOG_INFO("App", "Shutting down Unified gRPC Server...");
411 grpc_server_->Shutdown();
412 grpc_server_.reset();
413 }
414 canvas_automation_service_.reset();
415 test::TestManager::Get().SetFailureScreenshotRequester({});
416#endif
417
418#ifndef __EMSCRIPTEN__
419 // Clean up activity file for instance discovery
420 if (activity_file_) {
421 LOG_INFO("App", "Removing activity file: %s",
422 activity_file_->GetPath().c_str());
423 activity_file_.reset(); // Destructor deletes the file
424 }
425#endif
426
427 if (controller_) {
428 controller_->OnExit();
429 controller_.reset();
430 }
431}
432
433} // namespace yaze
Main application singleton managing lifecycle and global state.
Definition application.h:62
std::string pending_rom_
AppConfig config_
Definition application.h:99
static Application & Instance()
std::unique_ptr< Controller > controller_
Definition application.h:98
void LoadRom(const std::string &path)
auto filename() const
Definition rom.h:207
A class for emulating and debugging SNES games.
Definition emulator.h:41
Abstract interface for emulator backends (Internal vs Mesen2)
Definition i_emulator.h:23
static TestManager & Get()
#define YAZE_VERSION_STRING
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
Configuration options for the application startup.
Definition application.h:26
std::string rom_file
Definition application.h:28
std::string test_harness_bind_address
Definition application.h:52
StartupVisibility welcome_mode
Definition application.h:32
bool enable_test_harness
Definition application.h:51
std::string backend
Definition application.h:55
StartupVisibility dashboard_mode
Definition application.h:33
Status information for an active YAZE instance.