102 Patches: Detours to the Rescue
C reference for DttR maintainers and modders.
Loading...
Searching...
No Matches
imgui_overlay.c
Go to the documentation of this file.
1#ifdef DTTR_MODS_ENABLED
2
5
6#include <dttr_imgui.h>
7#include <dttr_log.h>
8
9#include <math.h>
10#include <stdio.h>
11#include <string.h>
12
13static DTTR_BackendType backend_type;
14static SDL_Window *window;
15static DTTR_ImGuiDesktopScaleState imgui_scale;
16static bool initialized;
17
18static const float modding_badge_font_factor = 0.90f;
19static const float modding_badge_min_font_size = 6.0f;
20static const float modding_badge_default_font_size = 13.0f;
21static const float modding_badge_line_advance_factor = 0.86f;
22static const float modding_badge_bold_offset = 0.5f;
23static const ImVec4_c modding_badge_header_color = {1.0f, 1.0f, 1.0f, 1.0f};
24static const ImVec4_c modding_badge_seconds_color = {0.35f, 1.0f, 0.35f, 1.0f};
25static const ImVec4_c modding_badge_hot_reload_on_color = {0.35f, 1.0f, 0.35f, 1.0f};
26static const ImVec4_c modding_badge_hot_reload_off_color = {1.0f, 0.35f, 0.35f, 1.0f};
27static const char modding_badge_hot_reload_label[] = "Hot Reload:";
28
29static float overlay_text_width(const char *text) {
30 if (!text) {
31 return 0.0f;
32 }
33
34 const ImVec2_c size = igCalcTextSize(text, NULL, false, -1.0f);
35 return size.x;
36}
37
38static float overlay_line_height() {
39 return igGetTextLineHeight();
40}
41
42static float overlay_line_advance() {
43 return igGetTextLineHeight() * modding_badge_line_advance_factor;
44}
45
46static float overlay_mod_gap() {
47 const ImGuiStyle *style = igGetStyle();
48 const float spacing = style ? style->ItemSpacing.x : 4.0f;
49 return spacing * 0.5f;
50}
51
52static void draw_overlay_text_at(
53 ImDrawList *draw_list,
54 ImVec2_c pos,
55 ImU32 color,
56 const char *text
57) {
58 if (!text) {
59 return;
60 }
61
62 ImDrawList_AddText_Vec2(draw_list, pos, color, text, text + strlen(text));
63}
64
65static void draw_bold_overlay_text(
66 ImDrawList *draw_list,
67 ImVec2_c pos,
68 ImU32 color,
69 const char *text
70) {
71 draw_overlay_text_at(draw_list, pos, color, text);
72 draw_overlay_text_at(
73 draw_list,
74 (ImVec2_c){pos.x + modding_badge_bold_offset, pos.y},
75 color,
76 text
77 );
78}
79
80static float hot_reload_width() {
81 const char *state = dttr_mods_hot_reload_enabled() ? "on" : "off";
82 return overlay_text_width(modding_badge_hot_reload_label) + overlay_mod_gap()
83 + overlay_text_width(state);
84}
85
86static void draw_hot_reload_header(float width) {
87 ImDrawList *draw_list = igGetWindowDrawList();
88 if (!draw_list) {
89 return;
90 }
91
92 const bool hot_reload_enabled = dttr_mods_hot_reload_enabled();
93 const char *state = hot_reload_enabled ? "on" : "off";
94 const ImVec4_c state_color = hot_reload_enabled ? modding_badge_hot_reload_on_color
95 : modding_badge_hot_reload_off_color;
96 const float gap = overlay_mod_gap();
97 const float label_width = overlay_text_width(modding_badge_hot_reload_label);
98 const ImVec2_c cursor = igGetCursorScreenPos();
99 const ImU32 label_color = igGetColorU32_Vec4(modding_badge_header_color);
100 const ImU32 state_color_u32 = igGetColorU32_Vec4(state_color);
101
102 draw_bold_overlay_text(draw_list, cursor, label_color, modding_badge_hot_reload_label);
103 draw_bold_overlay_text(
104 draw_list,
105 (ImVec2_c){cursor.x + label_width + gap, cursor.y},
106 state_color_u32,
107 state
108 );
109
110 igDummy((ImVec2_c){width, overlay_line_advance()});
111}
112
113static void mod_overlay_elapsed_text(char *out, size_t out_size, size_t mod_index) {
114 const unsigned long elapsed_seconds = (unsigned long)(dttr_mods_loaded_elapsed_ms(
115 mod_index
116 )
117 / 1000u);
118 snprintf(out, out_size, "(%lus)", elapsed_seconds);
119}
120
121static void draw_mod_overlay_row(
122 const char *name,
123 const char *seconds,
124 float gap,
125 float total_width
126) {
127 ImDrawList *draw_list = igGetWindowDrawList();
128 if (!draw_list) {
129 return;
130 }
131
132 const ImVec2_c cursor = igGetCursorScreenPos();
133 const ImU32 text_color = igGetColorU32_Col(ImGuiCol_Text, 1.0f);
134 const ImU32 seconds_color = igGetColorU32_Vec4(modding_badge_seconds_color);
135 const char *mod_name = name ? name : "";
136 const float name_width = overlay_text_width(mod_name);
137
138 draw_bold_overlay_text(draw_list, cursor, text_color, mod_name);
139 draw_overlay_text_at(
140 draw_list,
141 (ImVec2_c){cursor.x + name_width + gap, cursor.y},
142 seconds_color,
143 seconds
144 );
145
146 igDummy((ImVec2_c){total_width, overlay_line_advance()});
147}
148
149// Selects the SDL GPU ImGui backend only when the active renderer is SDL GPU.
150static bool uses_sdl_gpu() {
151 return backend_type == DTTR_BACKEND_SDL_GPU;
152}
153
154// Returns the ImGui backend label used in overlay startup logs.
155static const char *backend_name() {
156 return uses_sdl_gpu() ? "SDL_GPU" : "OpenGL";
157}
158
159// Skips backend submission when ImGui produced no command lists for this frame.
160static bool has_draw_data(const ImDrawData *draw_data) {
161 return draw_data && draw_data->CmdListsCount > 0;
162}
163
164// Initializes the ImGui renderer backend that matches the active sidecar graphics API.
165static void backend_init(SDL_Window *sdl_window, SDL_GPUDevice *device) {
166 if (uses_sdl_gpu()) {
167 ImGui_ImplSDL3_InitForSDLGPU(sdl_window);
168
169 SDL_GPUTextureFormat swapchain_format = SDL_GetGPUSwapchainTextureFormat(
170 device,
171 sdl_window
172 );
173 CImGui_ImplSDLGPU3_InitInfo info = {
174 .Device = device,
175 .ColorTargetFormat = swapchain_format,
176 .MSAASamples = SDL_GPU_SAMPLECOUNT_1,
177 .SwapchainComposition = SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
178 .PresentMode = SDL_GPU_PRESENTMODE_VSYNC,
179 };
180
181 cImGui_ImplSDLGPU3_Init(&info);
182 return;
183 }
184
185 ImGui_ImplSDL3_InitForOpenGL(sdl_window, NULL);
186 ImGui_ImplOpenGL3_Init("#version 330");
187}
188
189// Shuts down the renderer-specific ImGui backend before the shared SDL layer exits.
190static void backend_shutdown() {
191 if (uses_sdl_gpu()) {
192 cImGui_ImplSDLGPU3_Shutdown();
193 return;
194 }
195
196 ImGui_ImplOpenGL3_Shutdown();
197}
198
199// Creates the ImGui context and backend bindings used by mod overlay UI.
200void dttr_imgui_init(
201 SDL_Window *sdl_window,
202 SDL_GPUDevice *device,
203 DTTR_BackendType backend
204) {
205 if (initialized) {
207 "ImGui overlay init requested while already initialized; cleaning up stale "
208 "backend state"
209 );
211 }
212
213 backend_type = backend;
214 window = sdl_window;
215 imgui_scale = (DTTR_ImGuiDesktopScaleState){0};
216
217 igCreateContext(NULL);
218
219 ImGuiIO *io = igGetIO_Nil();
220 io->ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
221
222 igStyleColorsDark(NULL);
223
224 ImGuiStyle *style = igGetStyle();
225 style->Alpha = 0.9f;
226 style->WindowRounding = 4.0f;
227 DTTR_ImGui_ApplyWindowDesktopScale(&imgui_scale, window);
228
229 backend_init(window, device);
230 initialized = true;
231 DTTR_LOG_INFO("ImGui overlay initialized (backend: %s)", backend_name());
232}
233
234// Hold SDL events until the interactive overlay frame. igNewFrame() drains all
235// queued ImGui input, so feeding events during the no-input game-layer frame
236// would consume them before the overlay can react.
237#define DTTR_IMGUI_MAX_PENDING_EVENTS 256u
238static SDL_Event pending_events[DTTR_IMGUI_MAX_PENDING_EVENTS];
239static uint32_t pending_event_count;
240static bool pending_event_overflow_warned;
241
242// Feed buffered SDL events before the interactive overlay frame.
243static void flush_pending_events() {
244 for (uint32_t i = 0; i < pending_event_count; i++) {
245 ImGui_ImplSDL3_ProcessEvent(&pending_events[i]);
246 }
247
248 pending_event_count = 0;
249}
250
251// Destroys ImGui state and clears cached backend handles during graphics shutdown.
252void dttr_imgui_cleanup() {
253 if (!initialized) {
254 return;
255 }
256
257 backend_shutdown();
258 ImGui_ImplSDL3_Shutdown();
259 igDestroyContext(NULL);
260 window = NULL;
261 imgui_scale = (DTTR_ImGuiDesktopScaleState){0};
262 pending_event_count = 0;
263 pending_event_overflow_warned = false;
264 initialized = false;
265 DTTR_LOG_INFO("ImGui overlay cleaned up");
266}
267
268// Lets ImGui consume SDL mouse and keyboard events before they reach game input hooks.
270 if (!initialized) {
271 return false;
272 }
273
274 if (pending_event_count < DTTR_IMGUI_MAX_PENDING_EVENTS) {
275 pending_events[pending_event_count++] = *event;
276 } else {
277 // Deliver the event immediately rather than dropping it.
278 if (!pending_event_overflow_warned) {
279 pending_event_overflow_warned = true;
281 "ImGui pending event buffer full; delivering events immediately"
282 );
283 }
284
285 ImGui_ImplSDL3_ProcessEvent(event);
286 }
287
288 // Capture decisions use the previous interactive frame's hover state.
289 ImGuiIO *io = igGetIO_Nil();
290 switch (event->type) {
291 case SDL_EVENT_MOUSE_MOTION:
292 case SDL_EVENT_MOUSE_BUTTON_DOWN:
293 case SDL_EVENT_MOUSE_BUTTON_UP:
294 case SDL_EVENT_MOUSE_WHEEL:
295 return io->WantCaptureMouse;
296 case SDL_EVENT_KEY_DOWN:
297 case SDL_EVENT_KEY_UP:
298 case SDL_EVENT_TEXT_INPUT:
299 return io->WantCaptureKeyboard;
300 default:
301 return false;
302 }
303}
304
305// Starts a renderer-backend ImGui frame for the active graphics API.
306static void backend_new_frame() {
307 if (uses_sdl_gpu()) {
308 cImGui_ImplSDLGPU3_NewFrame();
309 } else {
310 ImGui_ImplOpenGL3_NewFrame();
311 }
312}
313
314// Starts a full interactive ImGui frame with SDL input and desktop scaling applied.
315static void new_frame() {
316 flush_pending_events();
317 backend_new_frame();
318 ImGui_ImplSDL3_NewFrame();
319 DTTR_ImGui_ApplyWindowDesktopScale(&imgui_scale, window);
320 igNewFrame();
321}
322
323// Starts an offscreen ImGui frame for game rendering callbacks that should not read
324// input.
325static void new_frame_no_input(uint32_t w, uint32_t h) {
326 backend_new_frame();
327 DTTR_ImGui_ApplyWindowDesktopScale(&imgui_scale, window);
328 ImGuiIO *io = igGetIO_Nil();
329 io->DisplaySize = (ImVec2_c){(float)w, (float)h};
330 igNewFrame();
331}
332
333// Render mod game-layer ImGui draw data.
334static ImDrawData *render_game_frame(uint32_t w, uint32_t h) {
335 new_frame_no_input(w, h);
336
338 .width = w,
339 .height = h,
340 .scale = (float)h / 480.0f,
341 };
342
344
345 igRender();
346 return igGetDrawData();
347}
348
349// Draws the small modding badge in game coordinates after mod UI has rendered.
350static void draw_modding_overlay(const DTTR_Mods_RenderContext *ctx) {
351 const float game_scale = ctx->scale > 0.0f ? ctx->scale : 1.0f;
352 const float desktop_scale = DTTR_ImGui_GetCurrentDesktopScale(&imgui_scale);
353 const float margin = 6.0f * game_scale * desktop_scale;
354 const ImVec2_c pos = {
355 (float)ctx->game_x + margin,
356 (float)ctx->game_y + margin,
357 };
358
359 const ImVec2_c pivot = {0.0f, 0.0f};
360
361 igSetNextWindowPos(pos, ImGuiCond_Always, pivot);
362 igPushStyleVar_Float(ImGuiStyleVar_WindowBorderSize, 0.0f);
363 igPushStyleVar_Float(ImGuiStyleVar_WindowRounding, 0.0f);
364 igPushStyleVar_Vec2(ImGuiStyleVar_WindowPadding, (ImVec2_c){0.0f, 0.0f});
365
366 const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
367 | ImGuiWindowFlags_NoBackground
368 | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav
369 | ImGuiWindowFlags_NoMove
370 | ImGuiWindowFlags_NoSavedSettings
371 | ImGuiWindowFlags_AlwaysAutoResize;
372
373 ImGuiIO *io = igGetIO_Nil();
374 ImFont *font = io ? io->FontDefault : NULL;
375
376 if (!font) {
377 font = igGetFont();
378 }
379
380 if (font) {
381 const ImGuiStyle *style = igGetStyle();
382 const float style_font_size = style ? style->FontSizeBase : 0.0f;
383
384 const float font_size = font->LegacySize > 0.0f ? font->LegacySize
385 : modding_badge_default_font_size;
386
387 const float base_font_size = style_font_size > 0.0f ? style_font_size : font_size;
388
389 const float scaled_font_size = base_font_size * modding_badge_font_factor
390 * game_scale;
391
392 const float badge_font_size = scaled_font_size > modding_badge_min_font_size
393 ? scaled_font_size
394 : modding_badge_min_font_size;
395
396 igPushFont(font, badge_font_size);
397 }
398
399 const size_t loaded_count = dttr_mods_loaded_count();
400 float rows_width = 0.0f;
401 const float gap = overlay_mod_gap();
402
403 for (size_t i = 0; i < loaded_count; i++) {
404 char seconds[32];
405 mod_overlay_elapsed_text(seconds, sizeof(seconds), i);
406
407 const char *name = dttr_mods_loaded_name(i);
408 const float row_width = overlay_text_width(name ? name : "") + gap
409 + overlay_text_width(seconds);
410 rows_width = fmaxf(rows_width, row_width);
411 }
412
413 const float text_width = fmaxf(hot_reload_width(), rows_width);
414
415 igSetNextWindowContentSize((ImVec2_c){text_width, 0.0f});
416
417 if (igBegin("##modding_overlay", NULL, flags)) {
418 draw_hot_reload_header(text_width);
419
420 for (size_t i = 0; i < loaded_count; i++) {
421 char seconds[32];
422 mod_overlay_elapsed_text(seconds, sizeof(seconds), i);
423
424 const char *name = dttr_mods_loaded_name(i);
425 draw_mod_overlay_row(name, seconds, gap, text_width);
426 }
427
428 igDummy((ImVec2_c){text_width, overlay_line_height() - overlay_line_advance()});
429 }
430
431 igEnd();
432 igPopStyleVar(3);
433
434 if (font) {
435 igPopFont();
436 }
437}
438
439// Render mod overlay ImGui draw data.
440static ImDrawData *render_overlay_frame(
441 uint32_t swap_w,
442 uint32_t swap_h,
443 uint32_t game_x,
444 uint32_t game_y,
445 uint32_t game_w,
446 uint32_t game_h
447) {
449 .window_w = swap_w,
450 .window_h = swap_h,
451 .game_x = game_x,
452 .game_y = game_y,
453 .game_w = game_w,
454 .game_h = game_h,
455 .scale = (float)game_h / 480.0f,
456 };
457
458 new_frame();
461 draw_modding_overlay(&ctx);
463
464 igRender();
465 return igGetDrawData();
466}
467
468// Reads ImGui's current display size for OpenGL paths that present to the default target.
469static void current_display_size(uint32_t *width, uint32_t *height) {
470 ImGuiIO *io = igGetIO_Nil();
471 *width = (uint32_t)io->DisplaySize.x;
472 *height = (uint32_t)io->DisplaySize.y;
473}
474
475// Submit ImGui draw data to the SDL GPU target.
476static void submit_sdl3gpu(
477 ImDrawData *draw_data,
478 SDL_GPUCommandBuffer *cmd,
479 SDL_GPUTexture *target
480) {
481 if (!has_draw_data(draw_data)) {
482 return;
483 }
484
485 cImGui_ImplSDLGPU3_PrepareDrawData(draw_data, cmd);
486
487 SDL_GPUColorTargetInfo color_target = {
488 .texture = target,
489 .load_op = SDL_GPU_LOADOP_LOAD,
490 .store_op = SDL_GPU_STOREOP_STORE,
491 };
492
493 SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, NULL);
494 if (!pass) {
495 return;
496 }
497
498 cImGui_ImplSDLGPU3_RenderDrawData(draw_data, cmd, pass);
499 SDL_EndGPURenderPass(pass);
500}
501
502// Submit ImGui draw data with OpenGL.
503static void submit_opengl(ImDrawData *draw_data) {
504 if (!has_draw_data(draw_data)) {
505 return;
506 }
507
508 ImGui_ImplOpenGL3_RenderDrawData(draw_data);
509}
510
511// Renders mod-provided game content into an SDL GPU render target.
513 SDL_GPUCommandBuffer *cmd,
514 SDL_GPUTexture *render_target,
515 uint32_t w,
516 uint32_t h
517) {
519 return;
520 }
521
522 submit_sdl3gpu(render_game_frame(w, h), cmd, render_target);
523}
524
525// Renders mod-provided game content through the OpenGL ImGui backend.
528 return;
529 }
530
531 uint32_t width = 0;
532 uint32_t height = 0;
533 current_display_size(&width, &height);
534 submit_opengl(render_game_frame(width, height));
535}
536
537// Renders the interactive overlay into the SDL GPU swapchain target.
539 SDL_GPUCommandBuffer *cmd,
540 SDL_GPUTexture *swapchain_tex,
541 uint32_t swap_w,
542 uint32_t swap_h,
543 uint32_t game_x,
544 uint32_t game_y,
545 uint32_t game_w,
546 uint32_t game_h
547) {
548 submit_sdl3gpu(
549 render_overlay_frame(swap_w, swap_h, game_x, game_y, game_w, game_h),
550 cmd,
551 swapchain_tex
552 );
553}
554
555// Renders the interactive overlay through OpenGL after the game viewport is known.
557 uint32_t game_x,
558 uint32_t game_y,
559 uint32_t game_w,
560 uint32_t game_h
561) {
562 uint32_t width = 0;
563 uint32_t height = 0;
564 current_display_size(&width, &height);
565 submit_opengl(render_overlay_frame(width, height, game_x, game_y, game_w, game_h));
566}
567
568#endif // DTTR_MODS_ENABLED
DTTR_Graphics_COM_Direct3DDevice7 DWORD block DTTR_Graphics_COM_Direct3DDevice7 DWORD block DTTR_Graphics_COM_Direct3DDevice7 void void void void DWORD f DTTR_Graphics_COM_Direct3DDevice7 DWORD idx float
const DTTR_BackendState * state
DTTR_Graphics_COM_Direct3DDevice7 void DWORD flags DWORD void DWORD DWORD color
void void * ctx
DTTR_Graphics_COM_Direct3DDevice7 void DWORD flags DWORD void DWORD flags
DTTR_Graphics_COM_DirectDraw7 *self DWORD DWORD h
DTTR_Graphics_COM_DirectDraw7 *self DWORD w
const DWORD size
void void DWORD HANDLE event
DTTR_Graphics_COM_DirectDrawSurface7 DWORD flags void NULL
DTTR_Graphics_COM_DirectDrawSurface7 DWORD DWORD void void DWORD flags DTTR_Graphics_COM_DirectDrawSurface7 void void *cb void * target
static float DTTR_ImGui_GetCurrentDesktopScale(const DTTR_ImGuiDesktopScaleState *state)
Definition dttr_imgui.h:38
static bool DTTR_ImGui_ApplyWindowDesktopScale(DTTR_ImGuiDesktopScaleState *state, SDL_Window *window)
Definition dttr_imgui.h:77
#define DTTR_LOG_WARN(...)
Definition dttr_log.h:30
#define DTTR_LOG_INFO(...)
Definition dttr_log.h:29
union SDL_Event SDL_Event
Definition dttr_mods.h:19
struct SDL_Window SDL_Window
Definition dttr_mods.h:18
DTTR_BackendType
@ DTTR_BACKEND_SDL_GPU
void dttr_imgui_render_game_sdl3gpu(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *render_target, uint32_t w, uint32_t h)
void dttr_imgui_cleanup()
void dttr_imgui_render_sdl3gpu(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain_tex, uint32_t swap_w, uint32_t swap_h, uint32_t game_x, uint32_t game_y, uint32_t game_w, uint32_t game_h)
void dttr_imgui_render_opengl(uint32_t game_x, uint32_t game_y, uint32_t game_w, uint32_t game_h)
bool dttr_imgui_process_event(const SDL_Event *event)
void dttr_imgui_init(SDL_Window *window, SDL_GPUDevice *device, DTTR_BackendType backend)
void dttr_imgui_render_game_opengl()
void dttr_mods_render_game(const DTTR_Mods_RenderGameContext *ctx)
Definition mods.c:751
void dttr_mods_render(const DTTR_Mods_RenderContext *ctx)
Definition mods.c:755
void dttr_mods_imgui_end(const DTTR_Mods_RenderContext *ctx)
Definition mods.c:650
size_t dttr_mods_loaded_count()
Definition mods.c:763
void dttr_mods_imgui_begin(const DTTR_Mods_RenderContext *ctx)
Definition mods.c:646
bool dttr_mods_has_render_game()
Definition mods.c:741
DWORD dttr_mods_loaded_elapsed_ms(size_t index)
Definition mods.c:776
const char * dttr_mods_loaded_name(size_t index)
Definition mods.c:767
bool dttr_mods_hot_reload_enabled()
Definition mods.c:784