yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sdl3_window_backend.cc
Go to the documentation of this file.
1// sdl3_window_backend.cc - SDL3 Window Backend Implementation
2
3// Only compile SDL3 backend when YAZE_USE_SDL3 is defined
4#ifdef YAZE_USE_SDL3
5
7
8#include <SDL3/SDL.h>
9
10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
14#include "app/gui/core/style.h"
16#include "imgui/backends/imgui_impl_sdl3.h"
17#include "imgui/backends/imgui_impl_sdlrenderer3.h"
18#include "imgui/imgui.h"
19#include "util/log.h"
20#include "util/platform_paths.h"
21
22namespace yaze {
23
24// Forward reference to the global resize flag defined in window.cc
25namespace core {
26extern bool g_window_is_resizing;
27}
28
29namespace platform {
30
31// Alias to core's resize flag for compatibility
32#define g_window_is_resizing yaze::core::g_window_is_resizing
33
34namespace {
35// Marks wake events ("YAZE" in ASCII) so other SDL_EVENT_USER users are not
36// mistaken for wakes.
37constexpr Sint32 kWakeEventCode = 0x59415A45;
38} // namespace
39
40bool PushWakeEvent() {
41 SDL_Event event;
42 SDL_zero(event);
43 event.type = SDL_EVENT_USER;
44 event.user.code = kWakeEventCode;
45 // SDL_PushEvent is thread-safe and returns true when the event was queued.
46 return SDL_PushEvent(&event);
47}
48
49bool IsWakeEvent(const SDL_Event& event) {
50 return event.type == SDL_EVENT_USER && event.user.code == kWakeEventCode;
51}
52
53SDL3WindowBackend::~SDL3WindowBackend() {
54 if (initialized_) {
55 Shutdown();
56 }
57}
58
59absl::Status SDL3WindowBackend::Initialize(const WindowConfig& config) {
60 if (initialized_) {
61 LOG_WARN("SDL3WindowBackend", "Already initialized, shutting down first");
62 RETURN_IF_ERROR(Shutdown());
63 }
64
65 // Initialize SDL3 subsystems
66 // Note: SDL3 removed SDL_INIT_TIMER (timer is always available)
67 if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS)) {
68 return absl::InternalError(
69 absl::StrFormat("SDL_Init failed: %s", SDL_GetError()));
70 }
71
72 // Determine window size
73 int screen_width = config.width;
74 int screen_height = config.height;
75
76 if (screen_width == 0 || screen_height == 0) {
77 // Auto-detect from display
78 // SDL3 uses SDL_GetPrimaryDisplay() and SDL_GetCurrentDisplayMode()
79 SDL_DisplayID display_id = SDL_GetPrimaryDisplay();
80 const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display_id);
81
82 if (mode) {
83 screen_width = static_cast<int>(mode->w * config.display_scale);
84 screen_height = static_cast<int>(mode->h * config.display_scale);
85 } else {
86 // Fallback to reasonable defaults
87 screen_width = 1280;
88 screen_height = 720;
89 LOG_WARN("SDL3WindowBackend",
90 "Failed to get display mode, using defaults: %dx%d",
91 screen_width, screen_height);
92 }
93 }
94
95 // Build window flags
96 // Note: SDL3 changed some flag names
97 SDL_WindowFlags flags = 0;
98 if (config.resizable) {
99 flags |= SDL_WINDOW_RESIZABLE;
100 }
101 if (config.maximized) {
102 flags |= SDL_WINDOW_MAXIMIZED;
103 }
104 if (config.fullscreen) {
105 flags |= SDL_WINDOW_FULLSCREEN;
106 }
107 if (config.high_dpi) {
108 flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY;
109 }
110 if (config.hidden) {
111 flags |= SDL_WINDOW_HIDDEN;
112 }
113
114 // Create window
115 // Note: SDL3 uses SDL_CreateWindow with different signature
116 SDL_Window* raw_window = SDL_CreateWindow(config.title.c_str(), screen_width,
117 screen_height, flags);
118
119 if (!raw_window) {
120 SDL_Quit();
121 return absl::InternalError(
122 absl::StrFormat("SDL_CreateWindow failed: %s", SDL_GetError()));
123 }
124
125 window_ = std::unique_ptr<SDL_Window, SDL3WindowDeleter>(raw_window);
126
127 // Allocate legacy audio buffer for backwards compatibility
128 const int audio_frequency = 48000;
129 const size_t buffer_size = (audio_frequency / 50) * 2; // Stereo PAL
130 audio_buffer_ = std::shared_ptr<int16_t>(new int16_t[buffer_size],
131 std::default_delete<int16_t[]>());
132
133 LOG_INFO("SDL3WindowBackend", "Initialized: %dx%d, audio buffer: %zu samples",
134 screen_width, screen_height, buffer_size);
135
136 initialized_ = true;
137 active_ = true;
138 return absl::OkStatus();
139}
140
141absl::Status SDL3WindowBackend::Shutdown() {
142 if (!initialized_) {
143 return absl::OkStatus();
144 }
145
146 // Shutdown ImGui if initialized
147 if (imgui_initialized_) {
148 ShutdownImGui();
149 }
150
151 // Shutdown graphics arena while renderer is still valid
152 LOG_INFO("SDL3WindowBackend", "Shutting down graphics arena...");
154
155 // Destroy window
156 if (window_) {
157 LOG_INFO("SDL3WindowBackend", "Destroying window...");
158 window_.reset();
159 }
160
161 // Quit SDL
162 LOG_INFO("SDL3WindowBackend", "Shutting down SDL...");
163 SDL_Quit();
164
165 initialized_ = false;
166 LOG_INFO("SDL3WindowBackend", "Shutdown complete");
167 return absl::OkStatus();
168}
169
170bool SDL3WindowBackend::PollEvent(WindowEvent& out_event) {
171 SDL_Event sdl_event;
172 if (SDL_PollEvent(&sdl_event)) {
173 // Let ImGui process the event first
174 if (imgui_initialized_) {
175 ImGui_ImplSDL3_ProcessEvent(&sdl_event);
176 }
177
178 // Convert to platform-agnostic event
179 out_event = ConvertSDL3Event(sdl_event);
180 out_event.has_native_event = true;
181 out_event.native_event = sdl_event;
182 return true;
183 }
184 return false;
185}
186
187bool SDL3WindowBackend::WaitForEvent(int timeout_ms) {
188 // A null event pointer waits without removing the event from the queue.
189 return SDL_WaitEventTimeout(nullptr, timeout_ms);
190}
191
192void SDL3WindowBackend::ProcessNativeEvent(void* native_event) {
193 if (native_event && imgui_initialized_) {
194 ImGui_ImplSDL3_ProcessEvent(static_cast<SDL_Event*>(native_event));
195 }
196}
197
198WindowEvent SDL3WindowBackend::ConvertSDL3Event(const SDL_Event& sdl_event) {
199 WindowEvent event;
200 event.type = WindowEventType::None;
201
202 switch (sdl_event.type) {
203 // =========================================================================
204 // Application Events
205 // =========================================================================
206 case SDL_EVENT_QUIT:
207 event.type = WindowEventType::Quit;
208 break;
209
210 // =========================================================================
211 // Keyboard Events
212 // Note: SDL3 uses event.key.key instead of event.key.keysym.sym
213 // =========================================================================
214 case SDL_EVENT_KEY_DOWN:
215 event.type = WindowEventType::KeyDown;
216 event.key_code = sdl_event.key.key;
217 event.scan_code = sdl_event.key.scancode;
218 UpdateModifierState();
219 event.key_shift = key_shift_;
220 event.key_ctrl = key_ctrl_;
221 event.key_alt = key_alt_;
222 event.key_super = key_super_;
223 break;
224
225 case SDL_EVENT_KEY_UP:
226 event.type = WindowEventType::KeyUp;
227 event.key_code = sdl_event.key.key;
228 event.scan_code = sdl_event.key.scancode;
229 UpdateModifierState();
230 event.key_shift = key_shift_;
231 event.key_ctrl = key_ctrl_;
232 event.key_alt = key_alt_;
233 event.key_super = key_super_;
234 break;
235
236 // =========================================================================
237 // Mouse Events
238 // Note: SDL3 uses float coordinates
239 // =========================================================================
240 case SDL_EVENT_MOUSE_MOTION:
241 event.type = WindowEventType::MouseMotion;
242 event.mouse_x = sdl_event.motion.x;
243 event.mouse_y = sdl_event.motion.y;
244 break;
245
246 case SDL_EVENT_MOUSE_BUTTON_DOWN:
248 event.mouse_x = sdl_event.button.x;
249 event.mouse_y = sdl_event.button.y;
250 event.mouse_button = sdl_event.button.button;
251 break;
252
253 case SDL_EVENT_MOUSE_BUTTON_UP:
255 event.mouse_x = sdl_event.button.x;
256 event.mouse_y = sdl_event.button.y;
257 event.mouse_button = sdl_event.button.button;
258 break;
259
260 case SDL_EVENT_MOUSE_WHEEL:
261 event.type = WindowEventType::MouseWheel;
262 event.wheel_x = sdl_event.wheel.x;
263 event.wheel_y = sdl_event.wheel.y;
264 break;
265
266 // =========================================================================
267 // Drop Events
268 // =========================================================================
269 case SDL_EVENT_DROP_FILE:
270 event.type = WindowEventType::DropFile;
271 if (sdl_event.drop.data) {
272 event.dropped_file = sdl_event.drop.data;
273 // Note: SDL3 drop.data is managed by SDL, don't free it
274 }
275 break;
276
277 // =========================================================================
278 // Window Events - SDL3 Major Change
279 // SDL3 no longer uses SDL_WINDOWEVENT with sub-types.
280 // Each window event type is now a top-level event.
281 // =========================================================================
282 case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
283 event.type = WindowEventType::Close;
284 break;
285
286 case SDL_EVENT_WINDOW_RESIZED:
287 event.type = WindowEventType::Resize;
288 event.window_width = sdl_event.window.data1;
289 event.window_height = sdl_event.window.data2;
290 is_resizing_ = true;
292 break;
293
294 case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
295 // This is the SDL3 equivalent of SDL_WINDOWEVENT_SIZE_CHANGED
296 event.type = WindowEventType::Resize;
297 event.window_width = sdl_event.window.data1;
298 event.window_height = sdl_event.window.data2;
299 is_resizing_ = true;
301 break;
302
303 case SDL_EVENT_WINDOW_MINIMIZED:
304 event.type = WindowEventType::Minimized;
305 is_resizing_ = false;
306 g_window_is_resizing = false;
307 break;
308
309 case SDL_EVENT_WINDOW_MAXIMIZED:
310 event.type = WindowEventType::Maximized;
311 break;
312
313 case SDL_EVENT_WINDOW_RESTORED:
314 event.type = WindowEventType::Restored;
315 is_resizing_ = false;
316 g_window_is_resizing = false;
317 break;
318
319 case SDL_EVENT_WINDOW_SHOWN:
320 event.type = WindowEventType::Shown;
321 is_resizing_ = false;
322 g_window_is_resizing = false;
323 break;
324
325 case SDL_EVENT_WINDOW_HIDDEN:
326 event.type = WindowEventType::Hidden;
327 is_resizing_ = false;
328 g_window_is_resizing = false;
329 break;
330
331 case SDL_EVENT_WINDOW_EXPOSED:
332 event.type = WindowEventType::Exposed;
333 is_resizing_ = false;
334 g_window_is_resizing = false;
335 break;
336
337 case SDL_EVENT_WINDOW_FOCUS_GAINED:
338 event.type = WindowEventType::FocusGained;
339 break;
340
341 case SDL_EVENT_WINDOW_FOCUS_LOST:
342 event.type = WindowEventType::FocusLost;
343 break;
344 }
345
346 return event;
347}
348
349void SDL3WindowBackend::UpdateModifierState() {
350 // SDL3 uses SDL_GetModState which returns SDL_Keymod
351 SDL_Keymod mod = SDL_GetModState();
352 key_shift_ = (mod & SDL_KMOD_SHIFT) != 0;
353 key_ctrl_ = (mod & SDL_KMOD_CTRL) != 0;
354 key_alt_ = (mod & SDL_KMOD_ALT) != 0;
355 key_super_ = (mod & SDL_KMOD_GUI) != 0;
356}
357
358WindowStatus SDL3WindowBackend::GetStatus() const {
359 WindowStatus status;
360 status.is_active = active_;
361 status.is_resizing = is_resizing_;
362
363 if (window_) {
364 SDL_WindowFlags flags = SDL_GetWindowFlags(window_.get());
365 status.is_minimized = (flags & SDL_WINDOW_MINIMIZED) != 0;
366 status.is_maximized = (flags & SDL_WINDOW_MAXIMIZED) != 0;
367 status.is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN) != 0;
368 status.is_focused = (flags & SDL_WINDOW_INPUT_FOCUS) != 0;
369
370 SDL_GetWindowSize(window_.get(), &status.width, &status.height);
371 }
372
373 return status;
374}
375
376void SDL3WindowBackend::GetSize(int* width, int* height) const {
377 if (window_) {
378 SDL_GetWindowSize(window_.get(), width, height);
379 } else {
380 if (width)
381 *width = 0;
382 if (height)
383 *height = 0;
384 }
385}
386
387void SDL3WindowBackend::SetSize(int width, int height) {
388 if (window_) {
389 SDL_SetWindowSize(window_.get(), width, height);
390 }
391}
392
393std::string SDL3WindowBackend::GetTitle() const {
394 if (window_) {
395 const char* title = SDL_GetWindowTitle(window_.get());
396 return title ? title : "";
397 }
398 return "";
399}
400
401void SDL3WindowBackend::SetTitle(const std::string& title) {
402 if (window_) {
403 SDL_SetWindowTitle(window_.get(), title.c_str());
404 }
405}
406
407void SDL3WindowBackend::ShowWindow() {
408 if (window_) {
409 SDL_ShowWindow(window_.get());
410 }
411}
412
413void SDL3WindowBackend::HideWindow() {
414 if (window_) {
415 SDL_HideWindow(window_.get());
416 }
417}
418
419bool SDL3WindowBackend::InitializeRenderer(gfx::IRenderer* renderer) {
420 if (!window_ || !renderer) {
421 return false;
422 }
423
424 if (renderer->GetBackendRenderer()) {
425 // Already initialized
426 return true;
427 }
428
429 return renderer->Initialize(window_.get());
430}
431
432absl::Status SDL3WindowBackend::InitializeImGui(gfx::IRenderer* renderer) {
433 if (imgui_initialized_) {
434 return absl::OkStatus();
435 }
436
437 if (!renderer) {
438 return absl::InvalidArgumentError("Renderer is null");
439 }
440
441 IMGUI_CHECKVERSION();
442 ImGui::CreateContext();
443
444 ImGuiIO& io = ImGui::GetIO();
446 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
447 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
448 // Note: ViewportsEnable is intentionally NOT set for SDL3 + SDL_Renderer.
449 // imgui_impl_sdlrenderer3 has no multi-viewport support, so windows dragged
450 // outside the main window would not render.
451
452 // Ensure macOS-style behavior (Cmd acts as Ctrl for shortcuts)
453#ifdef __APPLE__
454 io.ConfigMacOSXBehaviors = true;
455#endif
456
457 if (auto ini_path = util::PlatformPaths::GetImGuiIniPath(); ini_path.ok()) {
458 static std::string ini_path_str;
459 if (ini_path_str.empty()) {
460 ini_path_str = ini_path->string();
461 }
462 io.IniFilename = ini_path_str.c_str();
463 } else {
464 io.IniFilename = nullptr;
465 LOG_WARN("SDL3WindowBackend", "Failed to resolve ImGui ini path: %s",
466 ini_path.status().ToString().c_str());
467 }
468
469 // Initialize ImGui backends for SDL3
470 SDL_Renderer* sdl_renderer =
471 static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
472
473 if (!sdl_renderer) {
474 return absl::InternalError("Failed to get SDL renderer from IRenderer");
475 }
476
477 // Note: SDL3 uses different ImGui backend functions
478 if (!ImGui_ImplSDL3_InitForSDLRenderer(window_.get(), sdl_renderer)) {
479 return absl::InternalError("ImGui_ImplSDL3_InitForSDLRenderer failed");
480 }
481
482 if (!ImGui_ImplSDLRenderer3_Init(sdl_renderer)) {
483 ImGui_ImplSDL3_Shutdown();
484 return absl::InternalError("ImGui_ImplSDLRenderer3_Init failed");
485 }
486
487 // Load fonts
489
490 // Apply default style
492
493 imgui_initialized_ = true;
494 LOG_INFO("SDL3WindowBackend", "ImGui initialized successfully");
495 return absl::OkStatus();
496}
497
498void SDL3WindowBackend::ShutdownImGui() {
499 if (!imgui_initialized_) {
500 return;
501 }
502
503 LOG_INFO("SDL3WindowBackend", "Shutting down ImGui implementations...");
504 ImGui_ImplSDLRenderer3_Shutdown();
505 ImGui_ImplSDL3_Shutdown();
506
507 LOG_INFO("SDL3WindowBackend", "Destroying ImGui context...");
508 ImGui::DestroyContext();
509
510 imgui_initialized_ = false;
511}
512
513void SDL3WindowBackend::NewImGuiFrame() {
514 if (!imgui_initialized_) {
515 return;
516 }
517
518 ImGui_ImplSDLRenderer3_NewFrame();
519 ImGui_ImplSDL3_NewFrame();
520}
521
522void SDL3WindowBackend::RenderImGui(gfx::IRenderer* renderer) {
523 if (!imgui_initialized_) {
524 return;
525 }
526
527 // Finalize ImGui frame and render draw data
528 ImGui::Render();
529
530 if (renderer) {
531 SDL_Renderer* sdl_renderer =
532 static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
533 if (sdl_renderer) {
534 ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), sdl_renderer);
535 }
536 }
537
538 // Multi-viewport support
539 ImGuiIO& io = ImGui::GetIO();
540 if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
541 ImGui::UpdatePlatformWindows();
542 ImGui::RenderPlatformWindowsDefault();
543 }
544}
545
546} // namespace platform
547} // namespace yaze
548
549#endif // YAZE_USE_SDL3
void Shutdown()
Definition arena.cc:489
static Arena & Get()
Definition arena.cc:24
static absl::StatusOr< std::filesystem::path > GetImGuiIniPath()
Get the ImGui ini path for YAZE.
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
void ColorsYaze()
Definition style.cc:33
void ConfigureImGuiDiagnostics(ImGuiIO &io, ImGuiDiagnosticsBuild build)
Set Dear ImGui's error-recovery and ID-conflict options.
bool PushWakeEvent()
Push an SDL user event that only wakes the frame loop.
bool IsWakeEvent(const SDL_Event &event)
True for an event pushed by PushWakeEvent().
absl::Status LoadPackageFonts()
#define g_window_is_resizing
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22