shithub: m8c

Download patch

ref: 546fc01da819c76655da407b74699f8c77da998b
parent: da71b1fbb21ab3a4a7d0629f5c597ee03eff6227
author: Jonne Kokkonen <jonne.kokkonen@gmail.com>
date: Thu Apr 24 05:10:07 EDT 2025

Better scaling for fullscreen, deprecate devcontainer

Use a 2 pass scaling with an intermediate texture when integer scaling is turned off to get better quality

--- a/.devcontainer.json
+++ /dev/null
@@ -1,5 +1,0 @@
-{
-    "build": {
-        "dockerfile": "Dockerfile"
-    }
-}
\ No newline at end of file
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,9 +4,16 @@
 
 set(APP_NAME m8c)
 
-option(USE_LIBSERIALPORT "Use libserialport as a backend" ON)
+option(USE_LIBSERIALPORT "Use libserialport as a backend" OFF)
 option(USE_LIBUSB "Use libusb as a backend" OFF)
 option(USE_RTMIDI "Use RtMidi as a backend" OFF)
+
+# Enable USE_LIBSERIALPORT by default if neither USE_LIBUSB nor USE_RTMIDI are defined
+if (NOT USE_LIBUSB AND NOT USE_RTMIDI)
+    message(STATUS "Neither USE_LIBUSB nor USE_RTMIDI are enabled. Enabling USE_LIBSERIALPORT by default.")
+    set(USE_LIBSERIALPORT ON)
+endif ()
+
 
 file(GLOB m8c_SRC "src/*.h" "src/*.c" "src/backends/*.h" "src/backends/*.c" "src/fonts/*.h")
 
--- a/Dockerfile
+++ /dev/null
@@ -1,2 +1,0 @@
-FROM mcr.microsoft.com/devcontainers/cpp
-RUN apt-get update && apt-get install -y libsdl2-dev libserialport-dev
--- a/src/events.c
+++ b/src/events.c
@@ -18,6 +18,13 @@
   case SDL_EVENT_TERMINATING:
     ret_val = SDL_APP_SUCCESS;
     break;
+  case SDL_EVENT_WINDOW_RESIZED:
+  case SDL_EVENT_WINDOW_MOVED:
+    // If the window size is changed, some operating systems might need a little nudge to fix scaling
+    renderer_fix_texture_scaling_after_window_resize(&ctx->conf);
+    break;
+
+  // --- iOS specific events ---
   case SDL_EVENT_DID_ENTER_BACKGROUND:
     // iOS: Application entered into the background on iOS. About 5 seconds to stop things.
     SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Received SDL_EVENT_DID_ENTER_BACKGROUND");
@@ -40,11 +47,8 @@
       renderer_clear_screen();
       m8_resume_processing();
     }
-  case SDL_EVENT_WINDOW_RESIZED:
-  case SDL_EVENT_WINDOW_MOVED:
-    // If the window size is changed, some operating systems might need a little nudge to fix scaling
-    renderer_fix_texture_scaling_after_window_resize();
-    break;
+  case SDL_EVENT_FINGER_DOWN:
+
 
   // --- Input events ---
   case SDL_EVENT_GAMEPAD_ADDED:
--- a/src/input.h
+++ b/src/input.h
@@ -51,5 +51,6 @@
 void input_handle_key_up_event(const struct app_context *ctx, const SDL_Event *event);
 void input_handle_gamepad_button(struct app_context *ctx, SDL_GamepadButton button, bool pressed);
 void input_handle_gamepad_axis(const struct app_context *ctx, SDL_GamepadAxis axis, Sint16 value);
+void input_handle_finger_down(const struct app_context *ctx, const SDL_Event *event);
 
 #endif // INPUT_H
--- a/src/main.c
+++ b/src/main.c
@@ -31,7 +31,7 @@
     screensaver_initialized = screensaver_init();
   }
   screensaver_draw();
-  render_screen();
+  render_screen(&ctx->conf);
 
   // Poll for M8 device every second
   if (ctx->device_connected == 0 && SDL_GetTicks() - ticks_poll_device > 1000) {
@@ -45,14 +45,14 @@
         }
       }
 
-      const int m8_enabled = m8_enable_display(0);
+      const int m8_enabled = m8_enable_display(1);
       // Device was found; enable display and proceed to the main loop
       if (m8_enabled == 1) {
         ctx->app_state = RUN;
         ctx->device_connected = 1;
+        SDL_Delay(100); // Give the display time to initialize
         screensaver_destroy();
         screensaver_initialized = 0;
-        SDL_Delay(100);     // Give the device time to initialize
         m8_reset_display(); // Avoid display glitches.
       } else {
         SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Device not detected.");
@@ -87,10 +87,13 @@
   }
 
   config_params_s conf = config_initialize(*config_filename);
-#ifndef TARGET_OS_IOS
-  // It's not possible to edit the config on iOS, so let's go with the defaults
-  config_read(&conf);
-#endif
+  if (TARGET_OS_IOS == 0) {
+    // It's not possible to edit the config on iOS, so let's go with the defaults
+    config_read(&conf);
+  } else {
+    // Use integer scaling on iOS
+    conf.integer_scaling = 0;
+  }
   return conf;
 }
 
@@ -141,7 +144,7 @@
     } else if (result == DEVICE_FATAL_ERROR) {
       return SDL_APP_FAILURE;
     }
-    render_screen();
+    render_screen(&ctx->conf);
     break;
   }
 
@@ -168,7 +171,6 @@
 
   *appstate = ctx;
   ctx->app_state = INITIALIZE;
-
   ctx->conf = initialize_config(argc, argv, &ctx->preferred_device, &config_filename);
   ctx->device_connected =
       handle_m8_connection_init(ctx->conf.wait_for_device, ctx->preferred_device);
@@ -192,13 +194,13 @@
     return SDL_APP_FAILURE;
   }
 
-  if (ctx->device_connected && m8_enable_display(0)) {
+  if (ctx->device_connected && m8_enable_display(1)) {
     if (ctx->conf.audio_enabled) {
       audio_initialize(ctx->conf.audio_device_name, ctx->conf.audio_buffer_size);
     }
     ctx->app_state = RUN;
-    SDL_Delay(100); // Give the device time to initialize
-    m8_reset_display(); // Avoid display glitches.
+    render_screen(&ctx->conf);
+    m8_reset_display(); // Second reset to avoid display glitches.
   } else {
     SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Device not detected.");
     ctx->device_connected = 0;
--- a/src/render.c
+++ b/src/render.c
@@ -19,12 +19,13 @@
 
 #include <stdlib.h>
 
-SDL_Window *win;
-SDL_Renderer *rend;
-SDL_Texture *main_texture;
-SDL_Color global_background_color = (SDL_Color){.r = 0x00, .g = 0x00, .b = 0x00, .a = 0x00};
-SDL_RendererLogicalPresentation window_scaling_mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
-SDL_ScaleMode texture_scaling_mode = SDL_SCALEMODE_NEAREST;
+static SDL_Window *win;
+static SDL_Renderer *rend;
+static SDL_Texture *main_texture;
+static SDL_Texture *hd_texture = NULL;
+static SDL_Color global_background_color = (SDL_Color){.r = 0x00, .g = 0x00, .b = 0x00, .a = 0x00};
+static SDL_RendererLogicalPresentation window_scaling_mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
+static SDL_ScaleMode texture_scaling_mode = SDL_SCALEMODE_NEAREST;
 
 static uint32_t ticks_fps;
 static int fps;
@@ -44,79 +45,46 @@
 
 uint8_t fullscreen = 0;
 
-static uint8_t dirty = 0;
+static uint8_t dirty = 1;
 
-static void use_integer_scaling(const unsigned int use_integer_scaling) {
-  if (use_integer_scaling) {
-    window_scaling_mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
-    texture_scaling_mode = SDL_SCALEMODE_NEAREST;
-  } else {
-    window_scaling_mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
-    texture_scaling_mode = SDL_SCALEMODE_NEAREST;
-  }
-  renderer_fix_texture_scaling_after_window_resize();
-}
+// Creates an intermediate texture dynamically based on window size
+static void create_hd_texture() {
+  SDL_LogDebug(SDL_LOG_CATEGORY_RENDER, "Creating HD texture");
+  int window_width, window_height;
 
-// Initializes SDL and creates a renderer and required surfaces
-int renderer_initialize(config_params_s *conf) {
+  // Get the current window size
+  SDL_GetWindowSize(win, &window_width, &window_height);
 
-  // SDL documentation recommends this
-  atexit(SDL_Quit);
-
-  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) == false) {
-    SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "SDL_Init: %s", SDL_GetError());
-    return 0;
+  // Calculate the maximum integer scaling factor
+  int scale_factor = SDL_min(window_width / texture_width, window_height / texture_height);
+  if (scale_factor < 1) {
+    scale_factor = 1; // Ensure at least 1x scaling
   }
+  SDL_LogDebug(SDL_LOG_CATEGORY_RENDER, "HD texture scale factor: %d", scale_factor);
 
-  if (!SDL_CreateWindowAndRenderer("m8c", texture_width * 2, texture_height * 2,
-                                   SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY |
-                                       SDL_WINDOW_OPENGL | conf->init_fullscreen,
-                                   &win, &rend)) {
-    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window and renderer: %s",
-                    SDL_GetError());
-    return false;
-  }
+  // Calculate the HD texture size
+  const int hd_texture_width = texture_width * scale_factor;
+  const int hd_texture_height = texture_height * scale_factor;
+  SDL_LogDebug(SDL_LOG_CATEGORY_RENDER, "HD texture size: %dx%d", hd_texture_width,
+               hd_texture_height);
 
-  SDL_SetRenderVSync(rend, 1);
-
-  if (!SDL_SetRenderLogicalPresentation(rend, texture_width, texture_height, window_scaling_mode)) {
-    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't set renderer logical presentation: %s",
-                 SDL_GetError());
-    return false;
+  // Destroy any existing HD texture
+  if (hd_texture != NULL) {
+    SDL_DestroyTexture(hd_texture);
   }
 
-  main_texture = NULL;
-  main_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
-                                   texture_width, texture_height);
-
-  if (main_texture == NULL) {
-    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create texture: %s", SDL_GetError());
-    return false;
+  // Create a new HD texture with the calculated size
+  hd_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
+                                 hd_texture_width, hd_texture_height);
+  if (!hd_texture) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't create HD texture: %s", SDL_GetError());
   }
 
-  SDL_SetTextureScaleMode(main_texture, texture_scaling_mode);
-  use_integer_scaling(conf->integer_scaling);
-
-  SDL_SetRenderTarget(rend, main_texture);
-
-  SDL_SetRenderDrawColor(rend, global_background_color.r, global_background_color.g,
-                         global_background_color.b, global_background_color.a);
-
-  if (!SDL_RenderClear(rend)) {
-    SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Couldn't clear renderer: %s", SDL_GetError());
-    return 0;
+  // Optionally, set a linear scaling mode for smoother rendering
+  if (!SDL_SetTextureScaleMode(hd_texture, SDL_SCALEMODE_LINEAR)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set HD texture scale mode: %s",
+                    SDL_GetError());
   }
-
-  renderer_set_font_mode(0);
-
-  SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "1");
-
-  dirty = 1;
-
-  SDL_PumpEvents();
-  render_screen();
-
-  return 1;
 }
 
 static void change_font(struct inline_font *font) {
@@ -142,7 +110,15 @@
     SDL_SetWindowSize(win, texture_width * 2, texture_height * 2);
   }
 
-  SDL_DestroyTexture(main_texture);
+  if (hd_texture != NULL) {
+    SDL_DestroyTexture(hd_texture);
+    create_hd_texture(); // Create the texture dynamically based on window size
+  }
+
+  if (main_texture != NULL) {
+    SDL_DestroyTexture(main_texture);
+  }
+
   SDL_SetRenderLogicalPresentation(rend, texture_width, texture_height, window_scaling_mode);
   main_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
                                    texture_width, texture_height);
@@ -185,7 +161,12 @@
 void renderer_close(void) {
   SDL_LogDebug(SDL_LOG_CATEGORY_RENDER, "Closing renderer");
   inline_font_close();
-  SDL_DestroyTexture(main_texture);
+  if (main_texture != NULL) {
+    SDL_DestroyTexture(main_texture);
+  }
+  if (hd_texture != NULL) {
+    SDL_DestroyTexture(hd_texture);
+  }
   SDL_DestroyRenderer(rend);
   SDL_DestroyWindow(win);
 }
@@ -334,43 +315,187 @@
   dirty = 1;
 }
 
-void render_screen(void) {
-  if (dirty) {
-    dirty = 0;
+static void log_fps_stats(void) {
+  fps++;
 
-    if (!SDL_SetRenderTarget(rend, NULL)) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set renderer target to window: %s", SDL_GetError());
+  if (SDL_GetTicks() - ticks_fps > 5000) {
+    ticks_fps = SDL_GetTicks();
+    SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "%.1f fps\n", (float)fps / 5);
+    fps = 0;
+  }
+}
+
+// Initializes SDL and creates a renderer and required surfaces
+int renderer_initialize(config_params_s *conf) {
+
+  // SDL documentation recommends this
+  atexit(SDL_Quit);
+
+  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) == false) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "SDL_Init: %s", SDL_GetError());
+    return 0;
+  }
+
+  if (!SDL_CreateWindowAndRenderer("m8c", texture_width * 2, texture_height * 2,
+                                   SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY |
+                                       SDL_WINDOW_OPENGL | conf->init_fullscreen,
+                                   &win, &rend)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window and renderer: %s",
+                    SDL_GetError());
+    return false;
+  }
+
+  SDL_SetRenderVSync(rend, 1);
+
+  if (!SDL_SetRenderLogicalPresentation(rend, texture_width, texture_height, window_scaling_mode)) {
+    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't set renderer logical presentation: %s",
+                 SDL_GetError());
+    return false;
+  }
+
+  main_texture = NULL;
+  main_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
+                                   texture_width, texture_height);
+
+  if (main_texture == NULL) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create texture: %s", SDL_GetError());
+    return false;
+  }
+
+  SDL_SetTextureScaleMode(main_texture, texture_scaling_mode);
+
+  SDL_SetRenderTarget(rend, main_texture);
+
+  SDL_SetRenderDrawColor(rend, global_background_color.r, global_background_color.g,
+                         global_background_color.b, global_background_color.a);
+
+  if (!SDL_RenderClear(rend)) {
+    SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Couldn't clear renderer: %s", SDL_GetError());
+    return 0;
+  }
+
+  renderer_set_font_mode(0);
+
+  SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "1");
+
+  dirty = 1;
+
+  SDL_PumpEvents();
+  render_screen(conf);
+
+  return 1;
+}
+
+void render_screen(config_params_s *conf) {
+  if (!dirty) {
+    // No draw commands have been issued since the last function call, do nothing
+    return;
+  }
+
+  if (!conf) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "render_screen configuration parameter is NULL.");
+    return;
+  }
+
+  dirty = 0;
+
+  if (!SDL_SetRenderTarget(rend, NULL)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set renderer target to window: %s",
+                    SDL_GetError());
+  }
+
+  if (!SDL_SetRenderDrawColor(rend, global_background_color.r, global_background_color.g,
+                              global_background_color.b, global_background_color.a)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set render draw color: %s", SDL_GetError());
+  }
+
+  if (!SDL_RenderClear(rend)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't clear renderer: %s", SDL_GetError());
+  }
+
+  if (conf->integer_scaling) {
+    // Direct rendering with integer scaling
+    if (!SDL_RenderTexture(rend, main_texture, NULL, NULL)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't render texture: %s", SDL_GetError());
     }
+  } else {
 
-    if (!SDL_SetRenderDrawColor(rend, global_background_color.r, global_background_color.g,
-                           global_background_color.b, global_background_color.a)) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set render draw color: %s", SDL_GetError());
+    int window_width, window_height;
+    if (!SDL_GetWindowSize(win, &window_width, &window_height)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't get window size: %s", SDL_GetError());
     }
 
-    if (!SDL_RenderClear(rend)) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't clear renderer: %s", SDL_GetError());
+    // Determine the texture aspect ratio
+    const float texture_aspect_ratio = (float)texture_width / (float)texture_height;
+
+    // Determine the window aspect ratio
+    const float window_aspect_ratio = (float)window_width / (float)window_height;
+
+    // Ensure that HD texture exists
+    if (hd_texture == NULL) {
+      create_hd_texture(); // Create the texture dynamically based on window size
     }
 
-    if (!SDL_RenderTexture(rend, main_texture, NULL, NULL)) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't render texture: %s", SDL_GetError());
+    if (!SDL_SetRenderTarget(rend, hd_texture)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set HD render target: %s", SDL_GetError());
     }
 
-    if (!SDL_RenderPresent(rend)) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't present renderer: %s", SDL_GetError());
+    // Fill HD texture with BG color
+    SDL_SetRenderDrawColor(rend, global_background_color.r, global_background_color.g,
+                           global_background_color.b, global_background_color.a);
+
+    if (!SDL_RenderClear(rend)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't clear HD texture: %s", SDL_GetError());
     }
 
-    if (!SDL_SetRenderTarget(rend, main_texture)) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set renderer target to texture: %s", SDL_GetError());
+    // Render the main texture to hd_texture. It has the same aspect ratio, so a NULL rect works.
+    if (!SDL_RenderTexture(rend, main_texture, NULL, NULL)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't render main texture to HD texture: %s",
+                      SDL_GetError());
+    };
+
+    // Switch the render target back to the window
+    if (!SDL_SetRenderTarget(rend, NULL)) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't reset render target to window: %s",
+                      SDL_GetError());
     }
 
-    fps++;
+    float texture_width_hd, texture_height_hd;
+    SDL_GetTextureSize(hd_texture, &texture_width_hd, &texture_height_hd);
 
-    if (SDL_GetTicks() - ticks_fps > 5000) {
-      ticks_fps = SDL_GetTicks();
-      SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "%.1f fps\n", (float)fps / 5);
-      fps = 0;
+    SDL_FRect dest_rect;
+
+    if (window_aspect_ratio > texture_aspect_ratio) {
+      // Window is relatively wider than the texture
+      dest_rect.h = (float)window_height;
+      dest_rect.w = dest_rect.h * texture_aspect_ratio;
+      dest_rect.x = ((float)window_width - dest_rect.w) / 2.0f;
+      dest_rect.y = 0;
+      SDL_RenderTexture(rend, hd_texture, NULL, &dest_rect);
+    } else if (window_aspect_ratio < texture_aspect_ratio) {
+      // Window is relatively taller than the texture
+      dest_rect.w = (float)window_width;
+      dest_rect.h = dest_rect.w / texture_aspect_ratio;
+      dest_rect.x = 0;
+      dest_rect.y = ((float)window_height - dest_rect.h) / 2.0f;
+      // Render the HD texture with the calculated destination rectangle
+      SDL_RenderTexture(rend, hd_texture, NULL, &dest_rect);
+    } else {
+      // Window and texture aspect ratios match
+      SDL_RenderTexture(rend, hd_texture, NULL, NULL);
     }
   }
+
+  if (!SDL_RenderPresent(rend)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't present renderer: %s", SDL_GetError());
+  }
+
+  if (!SDL_SetRenderTarget(rend, main_texture)) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't set renderer target to texture: %s",
+                    SDL_GetError());
+  }
+
+  log_fps_stats();
 }
 
 int screensaver_init(void) {
@@ -377,7 +502,7 @@
   if (screensaver_initialized) {
     return 1;
   }
-  SDL_SetRenderTarget(rend,main_texture);
+  SDL_SetRenderTarget(rend, main_texture);
   renderer_set_font_mode(1);
   global_background_color.r = 0, global_background_color.g = 0, global_background_color.b = 0;
   fx_cube_init(rend, (SDL_Color){255, 255, 255, 255}, texture_width, texture_height,
@@ -396,9 +521,23 @@
   screensaver_initialized = 0;
 }
 
-void renderer_fix_texture_scaling_after_window_resize(void) {
+void renderer_fix_texture_scaling_after_window_resize(config_params_s *conf) {
   SDL_SetRenderTarget(rend, NULL);
-  SDL_SetRenderLogicalPresentation(rend, texture_width, texture_height, window_scaling_mode);
+  if (conf->integer_scaling) {
+    // SDL internal integer scaling works well for this purpose
+    SDL_SetRenderLogicalPresentation(rend, texture_width, texture_height,
+                                     SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
+  } else {
+    // Fullscreen scaling: use an intermediate texture with the highest possible integer size factor
+    if (hd_texture != NULL) {
+      SDL_DestroyTexture(hd_texture);
+      create_hd_texture();
+    }
+    // SDL forces black borders in letterbox mode, so in HD mode the texture scaling is manual
+    SDL_SetRenderLogicalPresentation(rend, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
+    SDL_SetTextureScaleMode(main_texture, SDL_SCALEMODE_NEAREST);
+    SDL_SetTextureScaleMode(hd_texture, SDL_SCALEMODE_LINEAR);
+  }
   SDL_SetTextureScaleMode(main_texture, texture_scaling_mode);
 }
 
--- a/src/render.h
+++ b/src/render.h
@@ -12,7 +12,7 @@
 int renderer_initialize(config_params_s *conf);
 void renderer_close(void);
 void renderer_set_font_mode(int mode);
-void renderer_fix_texture_scaling_after_window_resize(void);
+void renderer_fix_texture_scaling_after_window_resize(config_params_s *conf);
 void renderer_clear_screen(void);
 
 void draw_waveform(struct draw_oscilloscope_waveform_command *command);
@@ -21,7 +21,7 @@
 
 void set_m8_model(unsigned int model);
 
-void render_screen(void);
+void render_screen(config_params_s *conf);
 void toggle_fullscreen(void);
 void display_keyjazz_overlay(uint8_t show, uint8_t base_octave, uint8_t velocity);
 
--