yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sdl2_window_backend.cc
Go to the documentation of this file.
1// sdl2_window_backend.cc - SDL2 Window Backend Implementation
2
4
6
7#include <filesystem>
8
9#include "absl/status/status.h"
10#include "absl/strings/str_format.h"
13#include "app/gui/core/style.h"
15#include "imgui/backends/imgui_impl_sdl2.h"
16#include "imgui/backends/imgui_impl_sdlrenderer2.h"
17#include "imgui/imgui.h"
18#include "util/log.h"
19#include "util/platform_paths.h"
20
21namespace yaze {
22
23// Forward reference to the global resize flag defined in window.cc
24namespace core {
26}
27
28namespace platform {
29
30// Alias to core's resize flag for compatibility
31#define g_window_is_resizing yaze::core::g_window_is_resizing
32
33namespace {
34// Marks wake events ("YAZE" in ASCII) so other SDL_USEREVENT users, such as
35// SDL timers or future code, are not mistaken for wakes.
36constexpr Sint32 kWakeEventCode = 0x59415A45;
37} // namespace
38
40 SDL_Event event;
41 SDL_zero(event);
42 event.type = SDL_USEREVENT;
43 event.user.code = kWakeEventCode;
44 // SDL_PushEvent is thread-safe and returns 1 when the event was queued.
45 return SDL_PushEvent(&event) == 1;
46}
47
48bool IsWakeEvent(const SDL_Event& event) {
49 return event.type == SDL_USEREVENT && event.user.code == kWakeEventCode;
50}
51
57
58absl::Status SDL2WindowBackend::Initialize(const WindowConfig& config) {
59 if (initialized_) {
60 LOG_WARN("SDL2WindowBackend", "Already initialized, shutting down first");
62 }
63
64#ifdef _WIN32
65 // Without this, Windows bitmap-stretches the DPI-unaware window at 125-150%
66 // scaling. SDL reads the hint during video init.
67 SDL_SetHint(SDL_HINT_WINDOWS_DPI_SCALING, "1");
68#endif
69
70 // Initialize SDL2 subsystems
71 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER |
72 SDL_INIT_GAMECONTROLLER) != 0) {
73 return absl::InternalError(
74 absl::StrFormat("SDL_Init failed: %s", SDL_GetError()));
75 }
76
77 // Determine window size
78 int screen_width = config.width;
79 int screen_height = config.height;
80
81 if (screen_width == 0 || screen_height == 0) {
82 // Display bounds use window coordinates; with Windows DPI scaling the
83 // display mode is in pixels and would oversize the window.
84 SDL_Rect display_bounds;
85 if (SDL_GetDisplayBounds(0, &display_bounds) == 0) {
86 screen_width = static_cast<int>(display_bounds.w * config.display_scale);
87 screen_height = static_cast<int>(display_bounds.h * config.display_scale);
88 } else {
89 // Fallback to reasonable defaults
90 screen_width = 1280;
91 screen_height = 720;
92 LOG_WARN("SDL2WindowBackend",
93 "Failed to get display bounds, using defaults: %dx%d",
94 screen_width, screen_height);
95 }
96 }
97
98 // Build window flags
99 Uint32 flags = 0;
100 if (config.resizable) {
101 flags |= SDL_WINDOW_RESIZABLE;
102 }
103 if (config.maximized) {
104 flags |= SDL_WINDOW_MAXIMIZED;
105 }
106 if (config.fullscreen) {
107 flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
108 }
109 if (config.high_dpi) {
110 flags |= SDL_WINDOW_ALLOW_HIGHDPI;
111 }
112 if (config.hidden) {
113 flags |= SDL_WINDOW_HIDDEN;
114 }
115
116 // Create window
117 window_ = std::unique_ptr<SDL_Window, util::SDL_Deleter>(
118 SDL_CreateWindow(config.title.c_str(), SDL_WINDOWPOS_UNDEFINED,
119 SDL_WINDOWPOS_UNDEFINED, screen_width, screen_height,
120 flags),
122
123 if (!window_) {
124 SDL_Quit();
125 return absl::InternalError(
126 absl::StrFormat("SDL_CreateWindow failed: %s", SDL_GetError()));
127 }
128
129 // Allocate legacy audio buffer for backwards compatibility
130 const int audio_frequency = 48000;
131 const size_t buffer_size = (audio_frequency / 50) * 2; // Stereo PAL
132 audio_buffer_ = std::shared_ptr<int16_t>(new int16_t[buffer_size],
133 std::default_delete<int16_t[]>());
134
135 LOG_INFO("SDL2WindowBackend", "Initialized: %dx%d, audio buffer: %zu samples",
136 screen_width, screen_height, buffer_size);
137
138 initialized_ = true;
139 active_ = true;
140 return absl::OkStatus();
141}
142
144 if (!initialized_) {
145 return absl::OkStatus();
146 }
147
148 // Pause and close audio device if open
149 if (audio_device_ != 0) {
150 SDL_PauseAudioDevice(audio_device_, 1);
151 SDL_CloseAudioDevice(audio_device_);
152 audio_device_ = 0;
153 }
154
155 // Shutdown ImGui if initialized
156 if (imgui_initialized_) {
158 }
159
160 // Shutdown graphics arena while renderer is still valid
161 LOG_INFO("SDL2WindowBackend", "Shutting down graphics arena...");
163
164 // Destroy window
165 if (window_) {
166 LOG_INFO("SDL2WindowBackend", "Destroying window...");
167 window_.reset();
168 }
169
170 // Quit SDL
171 LOG_INFO("SDL2WindowBackend", "Shutting down SDL...");
172 SDL_Quit();
173
174 initialized_ = false;
175 LOG_INFO("SDL2WindowBackend", "Shutdown complete");
176 return absl::OkStatus();
177}
178
180 SDL_Event sdl_event;
181 if (SDL_PollEvent(&sdl_event)) {
182 // Let ImGui process the event first
183 if (imgui_initialized_) {
184 ImGui_ImplSDL2_ProcessEvent(&sdl_event);
185 }
186
187 // Convert to platform-agnostic event
188 out_event = ConvertSDL2Event(sdl_event);
189 out_event.has_native_event = true;
190 out_event.native_event = sdl_event;
191 return true;
192 }
193 return false;
194}
195
197 // A null event pointer waits without removing the event from the queue.
198 return SDL_WaitEventTimeout(nullptr, timeout_ms) == 1;
199}
200
202 if (native_event && imgui_initialized_) {
203 ImGui_ImplSDL2_ProcessEvent(static_cast<SDL_Event*>(native_event));
204 }
205}
206
208 WindowEvent event;
210
211 switch (sdl_event.type) {
212 case SDL_QUIT:
213 event.type = WindowEventType::Quit;
214 break;
215
216 case SDL_KEYDOWN:
217 event.type = WindowEventType::KeyDown;
218 event.key_code = sdl_event.key.keysym.sym;
219 event.scan_code = sdl_event.key.keysym.scancode;
221 event.key_shift = key_shift_;
222 event.key_ctrl = key_ctrl_;
223 event.key_alt = key_alt_;
224 event.key_super = key_super_;
225 break;
226
227 case SDL_KEYUP:
228 event.type = WindowEventType::KeyUp;
229 event.key_code = sdl_event.key.keysym.sym;
230 event.scan_code = sdl_event.key.keysym.scancode;
232 event.key_shift = key_shift_;
233 event.key_ctrl = key_ctrl_;
234 event.key_alt = key_alt_;
235 event.key_super = key_super_;
236 break;
237
238 case SDL_MOUSEMOTION:
239 event.type = WindowEventType::MouseMotion;
240 event.mouse_x = static_cast<float>(sdl_event.motion.x);
241 event.mouse_y = static_cast<float>(sdl_event.motion.y);
242 break;
243
244 case SDL_MOUSEBUTTONDOWN:
246 event.mouse_x = static_cast<float>(sdl_event.button.x);
247 event.mouse_y = static_cast<float>(sdl_event.button.y);
248 event.mouse_button = sdl_event.button.button;
249 break;
250
251 case SDL_MOUSEBUTTONUP:
253 event.mouse_x = static_cast<float>(sdl_event.button.x);
254 event.mouse_y = static_cast<float>(sdl_event.button.y);
255 event.mouse_button = sdl_event.button.button;
256 break;
257
258 case SDL_MOUSEWHEEL:
259 event.type = WindowEventType::MouseWheel;
260 event.wheel_x = static_cast<float>(sdl_event.wheel.x);
261 event.wheel_y = static_cast<float>(sdl_event.wheel.y);
262 break;
263
264 case SDL_DROPFILE:
265 event.type = WindowEventType::DropFile;
266 if (sdl_event.drop.file) {
267 event.dropped_file = sdl_event.drop.file;
268 SDL_free(sdl_event.drop.file);
269 }
270 break;
271
272 case SDL_WINDOWEVENT:
273 switch (sdl_event.window.event) {
274 case SDL_WINDOWEVENT_CLOSE:
275 event.type = WindowEventType::Close;
276 break;
277
278 case SDL_WINDOWEVENT_SIZE_CHANGED:
279 case SDL_WINDOWEVENT_RESIZED:
280 event.type = WindowEventType::Resize;
281 event.window_width = sdl_event.window.data1;
282 event.window_height = sdl_event.window.data2;
283 is_resizing_ = true;
285 break;
286
287 case SDL_WINDOWEVENT_MINIMIZED:
288 event.type = WindowEventType::Minimized;
289 is_resizing_ = false;
290 g_window_is_resizing = false;
291 break;
292
293 case SDL_WINDOWEVENT_MAXIMIZED:
294 event.type = WindowEventType::Maximized;
295 break;
296
297 case SDL_WINDOWEVENT_RESTORED:
298 event.type = WindowEventType::Restored;
299 is_resizing_ = false;
300 g_window_is_resizing = false;
301 break;
302
303 case SDL_WINDOWEVENT_SHOWN:
304 event.type = WindowEventType::Shown;
305 is_resizing_ = false;
306 g_window_is_resizing = false;
307 break;
308
309 case SDL_WINDOWEVENT_HIDDEN:
310 event.type = WindowEventType::Hidden;
311 is_resizing_ = false;
312 g_window_is_resizing = false;
313 break;
314
315 case SDL_WINDOWEVENT_EXPOSED:
316 event.type = WindowEventType::Exposed;
317 is_resizing_ = false;
318 g_window_is_resizing = false;
319 break;
320
321 case SDL_WINDOWEVENT_FOCUS_GAINED:
322 event.type = WindowEventType::FocusGained;
323 break;
324
325 case SDL_WINDOWEVENT_FOCUS_LOST:
326 event.type = WindowEventType::FocusLost;
327 break;
328 }
329 break;
330 }
331
332 return event;
333}
334
336 SDL_Keymod mod = SDL_GetModState();
337 key_shift_ = (mod & KMOD_SHIFT) != 0;
338 key_ctrl_ = (mod & KMOD_CTRL) != 0;
339 key_alt_ = (mod & KMOD_ALT) != 0;
340 key_super_ = (mod & KMOD_GUI) != 0;
341}
342
344 WindowStatus status;
345 status.is_active = active_;
346 status.is_resizing = is_resizing_;
347
348 if (window_) {
349 Uint32 flags = SDL_GetWindowFlags(window_.get());
350 status.is_minimized = (flags & SDL_WINDOW_MINIMIZED) != 0;
351 status.is_maximized = (flags & SDL_WINDOW_MAXIMIZED) != 0;
352 status.is_fullscreen =
353 (flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP)) != 0;
354 status.is_focused = (flags & SDL_WINDOW_INPUT_FOCUS) != 0;
355
356 SDL_GetWindowSize(window_.get(), &status.width, &status.height);
357 }
358
359 return status;
360}
361
362void SDL2WindowBackend::GetSize(int* width, int* height) const {
363 if (window_) {
364 SDL_GetWindowSize(window_.get(), width, height);
365 } else {
366 if (width)
367 *width = 0;
368 if (height)
369 *height = 0;
370 }
371}
372
373void SDL2WindowBackend::SetSize(int width, int height) {
374 if (window_) {
375 SDL_SetWindowSize(window_.get(), width, height);
376 }
377}
378
379std::string SDL2WindowBackend::GetTitle() const {
380 if (window_) {
381 const char* title = SDL_GetWindowTitle(window_.get());
382 return title ? title : "";
383 }
384 return "";
385}
386
387void SDL2WindowBackend::SetTitle(const std::string& title) {
388 if (window_) {
389 SDL_SetWindowTitle(window_.get(), title.c_str());
390 }
391}
392
394 if (window_) {
395 SDL_ShowWindow(window_.get());
396 }
397}
398
400 if (window_) {
401 SDL_HideWindow(window_.get());
402 }
403}
404
406 if (!window_ || !renderer) {
407 return false;
408 }
409
410 if (renderer->GetBackendRenderer()) {
411 // Already initialized
412 return true;
413 }
414
415 return renderer->Initialize(window_.get());
416}
417
419 if (imgui_initialized_) {
420 return absl::OkStatus();
421 }
422
423 if (!renderer) {
424 return absl::InvalidArgumentError("Renderer is null");
425 }
426
427 IMGUI_CHECKVERSION();
428 ImGui::CreateContext();
429
430 ImGuiIO& io = ImGui::GetIO();
432 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
433 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
434 // Note: ViewportsEnable is intentionally NOT set for SDL2 + SDL_Renderer
435 // It causes scaling issues on macOS Retina displays
436
437 // Ensure macOS-style behavior (Cmd acts as Ctrl for shortcuts)
438 // ImGui should set this automatically based on __APPLE__, but force it to be safe
439#ifdef __APPLE__
440 io.ConfigMacOSXBehaviors = true;
441 LOG_INFO("SDL2WindowBackend", "Enabled ConfigMacOSXBehaviors for macOS");
442#endif
443
444 if (auto ini_path = util::PlatformPaths::GetImGuiIniPath(); ini_path.ok()) {
445 static std::string ini_path_str;
446 if (ini_path_str.empty()) {
447 ini_path_str = ini_path->string();
448 }
449 io.IniFilename = ini_path_str.c_str();
450 } else {
451 io.IniFilename = nullptr;
452 LOG_WARN("SDL2WindowBackend", "Failed to resolve ImGui ini path: %s",
453 ini_path.status().ToString().c_str());
454 }
455
456 // Initialize ImGui backends
457 SDL_Renderer* sdl_renderer =
458 static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
459
460 if (!sdl_renderer) {
461 return absl::InternalError("Failed to get SDL renderer from IRenderer");
462 }
463
464 if (!ImGui_ImplSDL2_InitForSDLRenderer(window_.get(), sdl_renderer)) {
465 return absl::InternalError("ImGui_ImplSDL2_InitForSDLRenderer failed");
466 }
467
468 if (!ImGui_ImplSDLRenderer2_Init(sdl_renderer)) {
469 ImGui_ImplSDL2_Shutdown();
470 return absl::InternalError("ImGui_ImplSDLRenderer2_Init failed");
471 }
472
473 // Load fonts
475
476 // Apply default style
478
479 imgui_initialized_ = true;
480 LOG_INFO("SDL2WindowBackend", "ImGui initialized successfully");
481 return absl::OkStatus();
482}
483
485 if (!imgui_initialized_) {
486 return;
487 }
488
489 LOG_INFO("SDL2WindowBackend", "Shutting down ImGui implementations...");
491 ImGui_ImplSDL2_Shutdown();
492
493 LOG_INFO("SDL2WindowBackend", "Destroying ImGui context...");
494 ImGui::DestroyContext();
495
496 imgui_initialized_ = false;
497}
498
500 if (!imgui_initialized_) {
501 return;
502 }
503
505 ImGui_ImplSDL2_NewFrame();
506 // ImGui_ImplSDL2_NewFrame() automatically handles DisplaySize and
507 // DisplayFramebufferScale via ImGui_ImplSDL2_GetWindowSizeAndFramebufferScale()
508 // which uses SDL_GetRendererOutputSize() when renderer is available.
509}
510
512 if (!imgui_initialized_) {
513 return;
514 }
515
516 // Finalize ImGui frame and render draw data
517 ImGui::Render();
518
519 if (renderer) {
520 SDL_Renderer* sdl_renderer =
521 static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
522 if (sdl_renderer) {
523 // The SDL_Renderer backend scales clip rects but not vertices unless the
524 // renderer scale is set; SDL then scales both and the backend skips it.
525 ImDrawData* draw_data = ImGui::GetDrawData();
526 SDL_RenderSetScale(sdl_renderer, draw_data->FramebufferScale.x,
527 draw_data->FramebufferScale.y);
528 ImGui_ImplSDLRenderer2_RenderDrawData(draw_data, sdl_renderer);
529 SDL_RenderSetScale(sdl_renderer, 1.0f, 1.0f);
530 }
531 }
532
533 // Multi-viewport support
534 ImGuiIO& io = ImGui::GetIO();
535 if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
536 ImGui::UpdatePlatformWindows();
537 ImGui::RenderPlatformWindowsDefault();
538 }
539}
540
541} // namespace platform
542} // namespace yaze
void Shutdown()
Definition arena.cc:489
static Arena & Get()
Definition arena.cc:24
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
virtual bool Initialize(SDL_Window *window)=0
Initializes the renderer with a given window.
virtual void * GetBackendRenderer()=0
Provides an escape hatch to get the underlying, concrete renderer object.
void ShowWindow() override
Show the window.
absl::Status InitializeImGui(gfx::IRenderer *renderer) override
Initialize ImGui backends for this window/renderer combo.
void SetTitle(const std::string &title) override
Set window title.
absl::Status Shutdown() override
Shutdown the window backend and release resources.
bool PollEvent(WindowEvent &out_event) override
Poll and process pending events.
bool WaitForEvent(int timeout_ms) override
Block until an event is pending or the timeout passes.
WindowStatus GetStatus() const override
Get current window status.
void GetSize(int *width, int *height) const override
Get window dimensions.
void ProcessNativeEvent(void *native_event) override
Process a native SDL event (for ImGui integration)
bool InitializeRenderer(gfx::IRenderer *renderer) override
Initialize renderer for this window.
void SetSize(int width, int height) override
Set window dimensions.
std::shared_ptr< int16_t > audio_buffer_
absl::Status Initialize(const WindowConfig &config) override
Initialize the window backend with configuration.
void ShutdownImGui() override
Shutdown ImGui backends.
void RenderImGui(gfx::IRenderer *renderer) override
Render ImGui draw data (and viewports if enabled)
WindowEvent ConvertSDL2Event(const SDL_Event &sdl_event)
std::unique_ptr< SDL_Window, util::SDL_Deleter > window_
void NewImGuiFrame() override
Start a new ImGui frame.
void HideWindow() override
Hide the window.
std::string GetTitle() const override
Get window title.
static absl::StatusOr< std::filesystem::path > GetImGuiIniPath()
Get the ImGui ini path for YAZE.
void ImGui_ImplSDLRenderer2_NewFrame()
void ImGui_ImplSDLRenderer2_RenderDrawData(ImDrawData *draw_data, SDL_Renderer *renderer)
bool ImGui_ImplSDLRenderer2_Init(SDL_Renderer *renderer)
void ImGui_ImplSDLRenderer2_Shutdown()
#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
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Window configuration parameters.
Definition iwindow.h:24
Platform-agnostic window event data.
Definition iwindow.h:65
WindowEventType type
Definition iwindow.h:66
Window backend status information.
Definition iwindow.h:117
Deleter for SDL_Window and SDL_Renderer.
Definition sdl_deleter.h:19