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