shithub: m8c

Download patch

ref: 9a669c32d7bb6e5edee452dd3bfbcd1426b38ed5
parent: e11f4056d9188a2124a8dfb4dc2f95b5f196963c
author: laamaa <jonne.kokkonen@gmail.com>
date: Wed Sep 10 11:01:35 EDT 2025

modularize log overlay system, move implementations to dedicated file and simplify render path integration

--- /dev/null
+++ b/src/log_overlay.c
@@ -1,0 +1,272 @@
+// Copyright 2025 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#include "log_overlay.h"
+
+#include <SDL3/SDL.h>
+
+#include "SDL2_inprint.h"
+#include "fonts/fonts.h"
+
+#define LOG_BUFFER_MAX_LINES 512
+#define LOG_LINE_MAX_CHARS 256
+
+static SDL_Texture *overlay_texture = NULL;
+static int overlay_visible = 0;
+static int overlay_needs_redraw = 0;
+
+static char log_lines[LOG_BUFFER_MAX_LINES][LOG_LINE_MAX_CHARS];
+static int log_line_start = 0;
+static int log_line_count = 0;
+
+static SDL_LogOutputFunction prev_log_output_fn = NULL;
+static void *prev_log_output_userdata = NULL;
+static SDL_Mutex *log_mutex = NULL; // Mutex for protecting log buffer
+
+static void log_buffer_append_line(const char *line) {
+  if (line[0] == '\0') {
+    return;
+  }
+  // Protect buffer updates (can be called from non-main threads)
+  if (log_mutex)
+    SDL_LockMutex(log_mutex);
+  const int index = (log_line_start + log_line_count) % LOG_BUFFER_MAX_LINES;
+  SDL_strlcpy(log_lines[index], line, LOG_LINE_MAX_CHARS);
+  if (log_line_count < LOG_BUFFER_MAX_LINES) {
+    log_line_count++;
+  } else {
+    log_line_start = (log_line_start + 1) % LOG_BUFFER_MAX_LINES;
+  }
+  overlay_needs_redraw = 1;
+  if (log_mutex)
+    SDL_UnlockMutex(log_mutex);
+}
+
+static void sdl_log_capture(void *userdata, int category, SDL_LogPriority priority,
+                            const char *message) {
+  // Suppress unused variable warnings
+  (void)userdata;
+  (void)category;
+  (void)priority;
+
+  char formatted[LOG_LINE_MAX_CHARS];
+  SDL_snprintf(formatted, sizeof(formatted), ">%s", message ? message : "");
+  log_buffer_append_line(formatted);
+
+  if (prev_log_output_fn != NULL) {
+    prev_log_output_fn(prev_log_output_userdata, category, priority, message);
+  }
+}
+
+void log_overlay_init(void) {
+  // Create synchronization primitive before hooking log output
+  if (!log_mutex) {
+    log_mutex = SDL_CreateMutex();
+    if (!log_mutex) {
+      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create log mutex: %s", SDL_GetError());
+    }
+  }
+  SDL_GetLogOutputFunction(&prev_log_output_fn, &prev_log_output_userdata);
+  SDL_SetLogOutputFunction(sdl_log_capture, NULL);
+}
+
+void log_overlay_toggle(void) {
+  overlay_visible = !overlay_visible;
+  overlay_needs_redraw = 1;
+}
+
+int log_overlay_is_visible(void) { return overlay_visible; }
+
+void log_overlay_invalidate(void) {
+  if (overlay_texture != NULL) {
+    SDL_DestroyTexture(overlay_texture);
+    overlay_texture = NULL;
+  }
+  overlay_needs_redraw = 1;
+}
+
+void log_overlay_destroy(void) {
+  // Restore previous log output function
+  if (prev_log_output_fn) {
+    SDL_SetLogOutputFunction(prev_log_output_fn, prev_log_output_userdata);
+    prev_log_output_fn = NULL;
+    prev_log_output_userdata = NULL;
+  }
+  if (overlay_texture != NULL) {
+    SDL_DestroyTexture(overlay_texture);
+    overlay_texture = NULL;
+  }
+  // Destroy synchronization primitive
+  if (log_mutex) {
+    SDL_DestroyMutex(log_mutex);
+    log_mutex = NULL;
+  }
+  overlay_needs_redraw = 1;
+}
+
+void log_overlay_render(SDL_Renderer *renderer, int logical_texture_width,
+                        int logical_texture_height, SDL_ScaleMode scale_mode,
+                        int font_mode_current) {
+  if (!overlay_visible) {
+    return;
+  }
+  if (overlay_texture == NULL) {
+    overlay_texture =
+        SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
+                          logical_texture_width, logical_texture_height);
+    if (overlay_texture == NULL) {
+      SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Couldn't create log texture: %s", SDL_GetError());
+      return;
+    }
+    SDL_SetTextureBlendMode(overlay_texture, SDL_BLENDMODE_BLEND);
+    SDL_SetTextureScaleMode(overlay_texture, scale_mode);
+  }
+
+  // Only update the overlay texture when its contents changed.
+  if (overlay_needs_redraw) {
+    overlay_needs_redraw = 0;
+
+    // Take a snapshot of the log ring buffer so we can render without holding the mutex.
+    // This prevents data races and avoids deadlocks if rendering logs internally.
+    int local_count = 0;
+    int local_start = 0;
+    if (log_mutex)
+      SDL_LockMutex(log_mutex);
+    local_count = log_line_count;
+    local_start = log_line_start;
+
+    // Snapshot holds copies of each visible line in chronological order.
+    char(*snapshot)[LOG_LINE_MAX_CHARS] = NULL;
+    if (local_count > 0) {
+      snapshot = (char(*)[LOG_LINE_MAX_CHARS])SDL_calloc((size_t)local_count, sizeof(log_lines[0]));
+    }
+    if (snapshot) {
+      for (int i = 0; i < local_count; i++) {
+        const int idx = (local_start + i) % LOG_BUFFER_MAX_LINES;
+        SDL_strlcpy(snapshot[i], log_lines[idx], LOG_LINE_MAX_CHARS);
+      }
+    }
+    if (log_mutex)
+      SDL_UnlockMutex(log_mutex);
+
+    // Bind the overlay texture as the render target and clear it with a translucent background.
+    SDL_Texture *prev_target = SDL_GetRenderTarget(renderer);
+    if (!SDL_SetRenderTarget(renderer, overlay_texture)) {
+      SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to set render target: %s", SDL_GetError());
+    }
+
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 220);
+    SDL_RenderClear(renderer);
+
+    // Switch to a small font for the overlay; remember previous mode to restore later.
+    const int prev_font_mode = font_mode_current;
+    inline_font_close();
+    const struct inline_font *font_small = fonts_get(0);
+    if (font_small) {
+      inline_font_initialize(font_small);
+
+      // Layout calculations:
+      // - glyph_x/y = character cell size in pixels.
+      // - margin_x/y = inner padding around the overlay.
+      // - cols = how many characters fit per line (accounting for a 1px inter-glyph gap).
+      const int line_height = font_small->glyph_y + 1;
+      const int margin_x = 2;
+      const int margin_y = 1;
+      const int usable_width = logical_texture_width - (margin_x * 2);
+      const int cols = SDL_max(1, usable_width / (font_small->glyph_x + 1));
+
+      // Determine how many text rows fit on the screen vertically.
+      const int max_rows = (logical_texture_height - margin_y * 2) / line_height;
+
+      // We want to show the newest content; walk backwards over the snapshot to find
+      // which line (and intra-line character offset) should be the first visible row,
+      // so that the last max_rows rows are visible.
+      int rows_needed = max_rows;
+      int start_idx = 0;            // index in snapshot[] to start drawing from
+      size_t start_char_offset = 0; // per-line character offset (for wrapped lines)
+
+      if (local_count > 0) {
+        for (int n = local_count - 1; n >= 0 && rows_needed > 0; n--) {
+          if (!snapshot) break; // nullptr safety
+          const size_t len = SDL_strlen(snapshot[n]);
+          // How many wrapped rows this line consumes
+          const int rows_for_line = SDL_max(1, (int)((len + cols - 1) / cols));
+          if (rows_for_line >= rows_needed) {
+            // This line provides the first visible portion.
+            // Compute which character to start from so we only draw the last rows_needed rows.
+            const int offset = SDL_max(0, (int)len - rows_needed * cols);
+            start_idx = n;
+            start_char_offset = (size_t)offset;
+            break;
+          }
+          // Not enough rows on this line, include it fully and continue upwards.
+          rows_needed -= rows_for_line;
+          start_idx = n;
+          start_char_offset = 0;
+        }
+      }
+
+      // Render loop:
+      // - Iterate from start_idx to the newest item (end of snapshot).
+      // - For each line, draw it in chunks of `cols` characters (word-wrap by fixed width).
+      // - Stop when we run out of vertical space.
+      if (local_count > 0) {
+        int y = margin_y;
+        size_t offset = start_char_offset;
+
+        for (int cur = start_idx; cur < local_count && y < logical_texture_height; cur++) {
+          if (!snapshot || cur < 0 || cur >= local_count) {
+            break;
+          }
+
+          const char *s = snapshot[cur];
+          const size_t len = SDL_strlen(s);
+
+          for (size_t pos = offset; pos < len && y < logical_texture_height;) {
+            const Uint32 fg = 0xFFFFFF; // draw text in white
+            const size_t remaining = len - pos;
+
+            // Take up to `cols` characters for this visual row
+            size_t take = (size_t)cols < remaining ? (size_t)cols : remaining;
+
+            // Copy the slice into a temporary buffer for printing
+            char buf[LOG_LINE_MAX_CHARS];
+            if (take >= sizeof(buf)) {
+              take = sizeof(buf) - 1;
+            }
+            SDL_memcpy(buf, s + pos, take);
+            buf[take] = '\0';
+
+            // Draw the row and advance one line vertically
+            inprint(renderer, buf, margin_x, y, fg, fg);
+            y += line_height;
+            pos += take;
+          }
+
+          // After the first (possibly partial) slice of this line, subsequent lines start at 0
+          offset = 0;
+        }
+      }
+    } else {
+      SDL_LogError(SDL_LOG_CATEGORY_RENDER, "fonts_get(0) returned NULL");
+    }
+
+    // Restore previous font mode and previous render target.
+    inline_font_close();
+    inline_font_initialize(fonts_get(prev_font_mode));
+    SDL_SetRenderTarget(renderer, prev_target);
+
+    // Free the snapshot after rendering.
+    if (snapshot) {
+      SDL_free(snapshot);
+    }
+  }
+
+  // Composite the overlay texture to the current render target every frame while visible.
+  if (overlay_texture) {
+    if (!SDL_RenderTexture(renderer, overlay_texture, NULL, NULL)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't render log overlay texture: %s",
+                      SDL_GetError());
+    }
+  }
+}
--- /dev/null
+++ b/src/log_overlay.h
@@ -1,0 +1,33 @@
+// Copyright 2025 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#ifndef LOG_OVERLAY_H_
+#define LOG_OVERLAY_H_
+
+#include <SDL3/SDL.h>
+
+// Initialize SDL log capture to mirror messages into the in-app overlay buffer
+void log_overlay_init(void);
+
+// Toggle overlay visibility
+void log_overlay_toggle(void);
+
+// Return non-zero if the overlay is currently visible
+int log_overlay_is_visible(void);
+
+// Invalidate any cached resources (e.g., after texture size change)
+void log_overlay_invalidate(void);
+
+// Destroy internal resources used by the overlay
+void log_overlay_destroy(void);
+
+// Ensure the overlay texture is up to date and composite it to the current render target
+// font_mode_current is used to restore the caller's font after drawing
+void log_overlay_render(SDL_Renderer *renderer,
+			   int logical_texture_width,
+			   int logical_texture_height,
+			   SDL_ScaleMode texture_scaling_mode,
+			   int font_mode_current);
+
+#endif // LOG_OVERLAY_H_
+
--