ref: 2b1cdc00c92ebab8a194583fe4fb00225f55313b
parent: b960255025fac920d12f89ef3493fb09ba2eb40b
parent: 38b7ad4931b40882d9c6bcae506fe8e45b167811
author: Jonne Kokkonen <jonne.kokkonen@gmail.com>
date: Fri Sep 12 11:36:09 EDT 2025
Merge pull request #206 from laamaa/feature/config-ui Feature/config UI
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,7 +15,7 @@
endif ()
-file(GLOB m8c_SRC "src/*.h" "src/*.c" "src/backends/*.h" "src/backends/*.c" "src/fonts/*.h")
+file(GLOB m8c_SRC "src/*.h" "src/*.c" "src/backends/*.h" "src/backends/*.c" "src/fonts/*.h" "src/fonts/*.c")
set(MACOS_CONTENTS "${CMAKE_CURRENT_SOURCE_DIR}/package/macos/m8c.app/Contents") set(MACOS_ENTITLEMENTS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/package/macos/Entitlements.plist")--- a/README.md
+++ b/README.md
@@ -2,13 +2,15 @@
## Introduction
+*m8c* is a client for Dirtywave M8 tracker's headless mode. The application should be cross-platform ready and can be
+built in Linux, Windows (with MSYS2/MINGW64) and macOS.
+
The [Dirtywave M8 Tracker](https://dirtywave.com/products/m8-tracker) is a portable sequencer and synthesizer, featuring
8 tracks of assignable instruments such as FM, waveform synthesis, virtual analog, sample playback, and MIDI output. It
is powered by a [Teensy](https://www.pjrc.com/teensy/) micro-controller and inspired by the Gameboy
tracker [Little Sound DJ](https://www.littlesounddj.com/lsd/index.php).
-While Dirtywave makes new batches of units available on a regular basis, M8 is sometimes sold out due to the worldwide
-chip shortage and high demand of the unit. To fill this gap and and to allow users to freely test this wonderful
+While Dirtywave makes new batches of units available on a regular basis, M8 is sometimes sold out due to the high demand of the unit. To fill this gap and to allow users to freely test this wonderful
tracker, [Timothy Lamb](https://github.com/trash80) was kind enough to make
the [M8 Headless](https://github.com/Dirtywave/M8HeadlessFirmware) available to everyone.
@@ -16,13 +18,10 @@
purchasing the actual unit. You can check its availability [here](https://dirtywave.com/products/m8-tracker-model-02).
Meanwhile, you can also subscribe to Timothy Lamb's [Patreon](https://www.patreon.com/trash80).
-*m8c* is a client for Dirtywave M8 tracker's headless mode. The application should be cross-platform ready and can be
-built in Linux, Windows (with MSYS2/MINGW64) and Mac OS.
-
Many thanks to:
* Trash80: For the great M8 hardware and the original fonts that were converted to a bitmap for use in the
- progam.
+ program.
* driedfruit: For a wonderful little routine to blit inline bitmap
fonts, [SDL_inprint](https://github.com/driedfruit/SDL_inprint/)
* marcinbor85: For the slip handling routine, https://github.com/marcinbor85/slip
@@ -35,28 +34,42 @@
## Installation
-### Windows / MacOS
+### Quick Start
-There are prebuilt binaries available in the [releases section](https://github.com/laamaa/m8c/releases/) for Windows and
-recent versions of MacOS.
+1. Download the prebuilt binary for your platform from the [releases section](https://github.com/laamaa/m8c/releases/)
+2. Connect your M8 or Teensy (with headless firmware) to your computer
+3. Run the program—it should automatically detect your device
-When running the program for the first time on MacOS, it may not open as it is from an Unidentified Developer. You need
+### Windows
+
+There are prebuilt binaries available in the [releases section](https://github.com/laamaa/m8c/releases/) for Windows.
+
+When running the program for the first time on Windows, Windows Defender may show a warning about an unrecognized app. Click "More info" and then "Run anyway" to proceed.
+
+### macOS
+
+There are prebuilt binaries available in the [releases section](https://github.com/laamaa/m8c/releases/) for recent versions of macOS.
+
+When running the program for the first time on macOS, it may not open as it is from an Unidentified Developer. You need
to open it from the Applications Folder via Control+Click > Open then select Open from the popup menu.
### Linux
-There are packages available for Fedora Linux and NixOS, or you can build the program from source.
+There are packages available for NixOS, an AppImage for easy installation, or you can build the program from source.
-#### Fedora
+#### AppImage
-``` sh
-sudo dnf copr enable laamaa/m8c
-sudo dnf install m8c
-```
+An AppImage is available for Linux in the [releases section](https://github.com/laamaa/m8c/releases/). To use it:
+1. Download the `.AppImage` file from the releases
+2. Make it executable: `chmod +x m8c-*.AppImage`
+3. Run it: `./m8c-*.AppImage`
+
+The AppImage is portable and doesn't require installation - just download and run.
+
#### NixOS
-``` sh
+```sh
nix-env -iA m8c-stable -f https://github.com/laamaa/m8c/archive/refs/heads/main.tar.gz
```
@@ -74,25 +87,35 @@
##### Linux (Apt/Ubuntu)
-As of writing, there is no official SDL3 package yet in the Ubuntu repositories.
+For Ubuntu 25.04 and later, SDL3 packages are available in the official repositories:
+
+```sh
+sudo apt update && sudo apt install -y git gcc pkg-config make libserialport-dev libsdl3-dev
+```
+
+For older Ubuntu versions, there is no official SDL3 package yet in the Ubuntu repositories.
You'll likely need to build the library
yourself. https://github.com/libsdl-org/SDL/blob/main/docs/README-cmake.md#building-sdl-on-unix
+##### Windows (MSYS2/MINGW64)
+
+This assumes you have [MSYS2](https://www.msys2.org/) installed:
+
+```sh
+pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkg-config mingw-w64-x86_64-make mingw-w64-x86_64-SDL3 mingw-w64-x86_64-libserialport
```
-sudo apt update && sudo apt install -y git gcc pkg-config make libserialport-dev
-```
-##### MacOS
+##### macOS
This assumes you have [installed brew](https://docs.brew.sh/Installation)
-```
+```sh
brew update && brew install git gcc make sdl3 libserialport pkg-config
```
#### Download source code
-```
+```sh
mkdir code && cd code
git clone https://github.com/laamaa/m8c.git
```
@@ -99,7 +122,7 @@
#### Build the program
-```
+```sh
cd m8c
make
```
@@ -109,7 +132,7 @@
Connect the M8 or Teensy (with headless firmware) to your computer and start the program. It should automatically detect
your device.
-```
+```sh
./m8c
```
@@ -120,9 +143,12 @@
When you have multiple M8 devices connected and you want to choose a specific one or launch m8c multiple times, you can
get the list of devices by running
-```
+```sh
./m8c --list
+```
+Example output:
+```
2024-02-25 18:39:27.806 m8c[99838:4295527] INFO: Found M8 device: /dev/cu.usbmodem124709801
2024-02-25 18:39:27.807 m8c[99838:4295527] INFO: Found M8 device: /dev/cu.usbmodem121136001
```
@@ -129,7 +155,7 @@
And you can specify the preferred device by using
-```
+```sh
./m8c --dev /dev/cu.usbmodem124709801
```
@@ -137,12 +163,12 @@
## Keyboard mappings
-Default keys for controlling the progam:
+Default keys for controlling the program:
-* Up arrow = up
-* Down arrow = down
-* Left arrow = left
-* Right arrow = right
+* ↑ = up
+* ↓ = down
+* ← = left
+* → = right
* z / left shift = shift
* x / space = play
* a / left alt = opt
@@ -150,8 +176,8 @@
Additional controls:
-* Alt + enter = toggle full screen / windowed
-* Alt + F4 = quit program
+* Alt+Enter = toggle full screen / windowed
+* Alt+F4 = quit program
* Delete = opt+edit (deletes a row)
* Esc = toggle keyjazz on/off
* r / select+start+opt+edit = reset display (if glitches appear on the screen, use this)
@@ -165,35 +191,66 @@
When keyjazz is active, regular a/s/z/x keys are disabled. The base octave can be adjusted with numpad star/divide keys
and the velocity can be set
-* Numpad asterisk (\*): increase base octave
-* Numpad divide (/): decrease base ooctave
+* Numpad asterisk (*): increase base octave
+* Numpad divide (/): decrease base octave
* Numpad plus (+): increase velocity
* Numpad minus (-): decrease velocity
## Gamepads
-The program uses SDL's game controller system, which should make it work automagically with most gamepads.On startup,
-the program tries to load a SDL game controller database named gamecontrollerdb.txt from the same directory as the
+The program uses SDL's game controller system, which should make it work automatically with most gamepads. On startup,
+the program tries to load a SDL game controller database named `gamecontrollerdb.txt` from the same directory as the
config file. If your joypad doesn't work out of the box, you might need to create custom bindings to this file, for
example with [SDL2 Gamepad Tool](https://generalarcade.com/gamepadtool/).
+### Gamepad Configuration
+
+To configure your gamepad:
+
+1. Download the [SDL2 Gamepad Tool](https://generalarcade.com/gamepadtool/)
+2. Connect your gamepad and open the tool
+3. Create custom bindings and save them to `gamecontrollerdb.txt`
+4. Place the file in the same directory as your `config.ini` file
+
## Audio
-Experimental audio routing support can be enabled by setting the config value `"audio_enabled"` to `"true"`. The audio
-buffer size can also be tweaked from the config file for possible lower latencies.
-If the right audio device is not picked up by default, you can use a specific audio device by using
-`"audio_device_name"` config parameter.
+m8c supports audio routing from the M8 device to your computer's audio output.
-It is possible to toggle audio routing on/off with a key defined in the config (`"key_toggle_audio"`). The default key
-is F12.
+### Audio Controls
-On MacOS you need to grant the program permission to access the Microphone for audio routing to work.
+- **Toggle audio routing:** F12 (default) or configure `key_toggle_audio` in config
+- **Audio buffer size:** Configure `audio_buffer_size` in config (0 = SDL default)
+- **Audio device:** Configure `audio_device_name` in config for specific device selection
+### Platform-specific Notes
+
+- **macOS:** Grant microphone permission for audio routing to work
+- **Linux:** May require additional audio permissions or configuration
+- **Windows:** Should work with standard audio drivers
+
## Config
+### Settings menu (in-app)
+
+You can change the most common options without editing `config.ini` using the in-app settings overlay.
+
+- **How to open:**
+ - Keyboard: press F1.
+ - Gamepad: hold the Back/Select button for about 2 seconds.
+- **How to navigate:**
+ - Move: Up/Down arrows or D‑pad.
+ - Activate/enter: Enter/Space or South/A.
+ - Adjust values (sliders/integers): Left/Right arrows or D‑pad left/right.
+ - Back/close: Esc or F1; on gamepad use East/B or Back.
+ - While remapping inputs, the menu will prompt you; press the desired key/button or move an axis. Use Esc/B/Back to cancel a capture.
+
+Changes take effect immediately; use Save if you want them persisted to disk.
+
+### Settings file (config.ini)
+
Application settings and keyboard/game controller bindings can be configured via `config.ini`.
-The keyboard configuration uses SDL2 Scancodes, a reference list can be found
+The keyboard configuration uses SDL Scancodes, a reference list can be found
at https://wiki.libsdl.org/SDL2/SDLScancodeLookup
If the file does not exist, it will be created in one of these locations:
@@ -200,11 +257,11 @@
* Windows: `C:\Users\<username>\AppData\Roaming\m8c\config.ini`
* Linux: `/home/<username>/.local/share/m8c/config.ini`
-* MacOS: `/Users/<username>/Library/Application Support/m8c/config.ini`
+* macOS: `/Users/<username>/Library/Application Support/m8c/config.ini`
You can choose to load an alternate configuration with the `--config` command line option. Example:
-```
+```sh
m8c --config alternate_config.ini
```
@@ -215,7 +272,7 @@
An in-app log overlay is available for platforms where reading console output is inconvenient.
-- Default toggle key: \` (grave). You can change it in `config.ini` under `[keyboard]` using `key_toggle_log=<SDL_SCANCODE>`.
+- Default toggle key: ` (grave). You can change it in `config.ini` under `[keyboard]` using `key_toggle_log=<SDL_SCANCODE>`.
- The overlay shows recent `SDL_Log*` messages.
- Long lines are wrapped to fit; the view tails the most recent output.
@@ -225,9 +282,11 @@
## FAQ
+### Permission Issues
+
* When starting the program, something like the following appears and the program does not start:
-```
+```sh
$ ./m8c
INFO: Looking for USB serial devices.
INFO: Found M8 in /dev/ttyACM1.
@@ -235,12 +294,12 @@
ERROR: Error: Failed: Permission denied
```
-This is likely caused because the user running m8c does not have permission to use the serial port. The eaiest way to
+This is likely caused because the user running m8c does not have permission to use the serial port. The easiest way to
fix this is to add the current user to a group with permission to use the serial port.
On Linux systems, look at the permissions on the serial port shown on the line that says "Found M8 in":
-```
+```sh
$ ls -la /dev/ttyACM1
crw-rw---- 1 root dialout 166, 0 Jan 8 14:51 /dev/ttyACM0
```
@@ -249,7 +308,7 @@
read/write permissions. To add a user to the group, run this command, replacing 'dialout' with the group shown on your
own system:
-``` sh
+```sh
sudo adduser $USER dialout
```
@@ -256,5 +315,17 @@
You may need to log out and back in or even fully reboot the system for this change to take effect, but this will
hopefully fix the problem. Please see [this issue for more details](https://github.com/laamaa/m8c/issues/20).
+### Device Not Found
+* The program starts but shows "No M8 device found":
+ - Ensure your M8 or Teensy is connected via USB
+ - Check that the headless firmware is properly installed
+ - Try running with `--list` to see detected devices
+ - On Linux, verify USB permissions (see permission issues above)
+### Audio Issues
+
+* No audio output:
+ - Check that audio routing is enabled (F12)
+ - Verify audio device selection in config
+ - On macOS, ensure microphone permission is granted
\ No newline at end of file
--- a/src/SDL2_inprint.h
+++ b/src/SDL2_inprint.h
@@ -4,10 +4,10 @@
#ifndef SDL2_inprint_h
#define SDL2_inprint_h
-#include "fonts/inline_font.h"
+#include "fonts/fonts.h"
#include <SDL3/SDL.h>
-extern void inline_font_initialize(struct inline_font *font);
+extern void inline_font_initialize(const struct inline_font *font);
extern void inline_font_close(void);
extern void inline_font_set_renderer(SDL_Renderer *renderer);
@@ -17,6 +17,6 @@
extern void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y, Uint32 fgcolor,
Uint32 bgcolor);
-extern SDL_Texture *get_inline_font(void);
+const struct inline_font *inline_font_get_current(void);
#endif /* SDL2_inprint_h */
--- a/src/common.h
+++ b/src/common.h
@@ -2,6 +2,12 @@
#define COMMON_H_
#include "config.h"
+// On MacOS TARGET_OS_IOS is defined as 0, so make sure that it's consistent on other platforms as
+// well
+#ifndef TARGET_OS_IOS
+#define TARGET_OS_IOS 0
+#endif
+
enum app_state { QUIT, INITIALIZE, WAIT_FOR_DEVICE, RUN }; struct app_context {--- a/src/config.h
+++ b/src/config.h
@@ -65,4 +65,7 @@
void read_key_config(const ini_t *ini, config_params_s *conf);
void read_gamepad_config(const ini_t *ini, config_params_s *conf);
+// Expose write so settings UI can persist changes
+void write_config(const config_params_s *conf);
+
#endif
--- a/src/events.c
+++ b/src/events.c
@@ -4,9 +4,12 @@
#include "gamepads.h"
#include "input.h"
#include "render.h"
+#include "settings.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
+static Uint64 g_back_pressed_at = 0;
+
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {struct app_context *ctx = appstate;
SDL_AppResult ret_val = SDL_APP_CONTINUE;
@@ -57,22 +60,64 @@
break;
case SDL_EVENT_KEY_DOWN:
+ // Toggle settings with F1
+ if (event->key.key == SDLK_F1 && event->key.repeat == 0) {+ settings_toggle_open();
+ return ret_val;
+ }
+ // Route to settings if open
+ if (settings_is_open()) {+ settings_handle_event(ctx, event);
+ return ret_val;
+ }
input_handle_key_down_event(ctx, event);
break;
case SDL_EVENT_KEY_UP:
+ if (settings_is_open()) {+ settings_handle_event(ctx, event);
+ return ret_val;
+ }
input_handle_key_up_event(ctx, event);
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
+ // Start measuring hold time for GUIDE; trigger handled on button up after 1s hold
+ if (event->gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {+ g_back_pressed_at = SDL_GetTicks();
+ return ret_val;
+ }
+
+ if (settings_is_open()) {+ settings_handle_event(ctx, event);
+ return ret_val;
+ }
input_handle_gamepad_button(ctx, event->gbutton.button, true);
break;
case SDL_EVENT_GAMEPAD_BUTTON_UP:
+ // Handle GUIDE release: toggle settings if held for at least 1 second
+ if (event->gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {+ const Uint64 now = SDL_GetTicks();
+ if (g_back_pressed_at != 0 && (now - g_back_pressed_at) >= 2000) {+ settings_toggle_open();
+ }
+ g_back_pressed_at = 0;
+ return ret_val;
+ }
+
+ if (settings_is_open()) {+ settings_handle_event(ctx, event);
+ return ret_val;
+ }
input_handle_gamepad_button(ctx, event->gbutton.button, false);
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
+ if (settings_is_open()) {+ settings_handle_event(ctx, event);
+ return ret_val;
+ }
input_handle_gamepad_axis(ctx, event->gaxis.axis, event->gaxis.value);
break;
--- a/src/fonts/font1.h
+++ b/src/fonts/font1.h
@@ -1,7 +1,7 @@
#ifndef FONT1_H_
#define FONT1_H_
-#include "inline_font.h"
+#include "fonts.h"
struct inline_font font_v1_small = {470,
--- a/src/fonts/font2.h
+++ b/src/fonts/font2.h
@@ -1,6 +1,7 @@
#ifndef FONT2_H_
#define FONT2_H_
-#include "inline_font.h"
+
+#include "fonts.h"
struct inline_font font_v1_large = {752,
--- a/src/fonts/font3.h
+++ b/src/fonts/font3.h
@@ -1,7 +1,7 @@
#ifndef FONT3_H_
#define FONT3_H_
-#include "inline_font.h"
+#include "fonts.h"
struct inline_font font_v2_small = {846,
--- a/src/fonts/font4.h
+++ b/src/fonts/font4.h
@@ -1,7 +1,7 @@
#ifndef FONT4_H_
#define FONT4_H_
-#include "inline_font.h"
+#include "fonts.h"
struct inline_font font_v2_large = {940,
--- a/src/fonts/font5.h
+++ b/src/fonts/font5.h
@@ -1,7 +1,7 @@
#ifndef FONT5_H_
#define FONT5_H_
-#include "inline_font.h"
+#include "fonts.h"
struct inline_font font_v2_huge = {1128,
--- /dev/null
+++ b/src/fonts/fonts.c
@@ -1,0 +1,29 @@
+// fonts.c
+#include "fonts.h"
+#include "font1.h"
+#include "font2.h"
+#include "font3.h"
+#include "font4.h"
+#include "font5.h"
+
+#include <stddef.h>
+
+static struct inline_font *fonts_storage[] = {+ &font_v1_small, &font_v1_large, &font_v2_small, &font_v2_large, &font_v2_huge
+};
+
+#define FONT_COUNT (sizeof(fonts_storage) / sizeof(fonts_storage[0]))
+
+size_t fonts_count(void) {+ return FONT_COUNT;
+}
+
+const struct inline_font *fonts_get(const size_t index) {+ return (index < FONT_COUNT) ? (const struct inline_font *)fonts_storage[index] : NULL;
+}
+
+const struct inline_font *const *fonts_all(size_t *count) {+ if (count) *count = FONT_COUNT;
+ // Cast to a read-only view so callers can’t mutate the array or elements
+ return (const struct inline_font *const *)fonts_storage;
+}
\ No newline at end of file
--- /dev/null
+++ b/src/fonts/fonts.h
@@ -1,0 +1,29 @@
+// fonts.h
+#ifndef FONTS_H
+#define FONTS_H
+
+#include <stddef.h>
+
+struct inline_font {+ const int width;
+ const int height;
+ const int glyph_x;
+ const int glyph_y;
+ const int screen_offset_x;
+ const int screen_offset_y;
+ const int text_offset_y;
+ const int waveform_max_height;
+ const long image_size;
+ const unsigned char image_data[];
+};
+
+// Number of available fonts
+size_t fonts_count(void);
+
+// Get a font by index (returns NULL if index is out of range)
+const struct inline_font *fonts_get(size_t index);
+
+// Get the whole font table (read-only). If count != NULL, it receives the length.
+const struct inline_font *const *fonts_all(size_t *count);
+
+#endif // FONTS_H
\ No newline at end of file
--- a/src/fonts/inline_font.h
+++ /dev/null
@@ -1,17 +1,0 @@
-#ifndef INLINE_FONT_H_
-#define INLINE_FONT_H_
-
-struct inline_font {- const int width;
- const int height;
- const int glyph_x;
- const int glyph_y;
- const int screen_offset_x;
- const int screen_offset_y;
- const int text_offset_y;
- const int waveform_max_height;
- const long image_size;
- const unsigned char image_data[];
-};
-
-#endif
--- a/src/inprint2.c
+++ b/src/inprint2.c
@@ -2,7 +2,7 @@
// https://github.com/driedfruit/SDL_inprint Released into public domain.
// Modified to support multiple fonts & adding a background to text.
-#include "fonts/inline_font.h"
+#include "fonts/fonts.h"
#include <SDL3/SDL.h>
#define CHARACTERS_PER_ROW 94
@@ -14,18 +14,18 @@
static SDL_Renderer *selected_renderer = NULL;
static SDL_Texture *inline_font = NULL;
static SDL_Texture *selected_font = NULL;
-static struct inline_font *selected_inline_font;
+static const struct inline_font *selected_inline_font;
static Uint16 selected_font_w, selected_font_h;
-void inline_font_initialize(struct inline_font *font) {+void inline_font_initialize(const struct inline_font *font) { if (inline_font != NULL) {return;
}
- selected_font_w = font->width;
- selected_font_h = font->height;
selected_inline_font = font;
+ selected_font_w = selected_inline_font->width;
+ selected_font_h = selected_inline_font->height;
SDL_IOStream *font_bmp =
SDL_IOFromConstMem(selected_inline_font->image_data, selected_inline_font->image_size);
@@ -132,4 +132,7 @@
d_rect.x += (float)selected_inline_font->glyph_x + 1;
}
}
-SDL_Texture *get_inline_font(void) { return selected_font; }+
+const struct inline_font *inline_font_get_current(void) {+ return selected_inline_font;
+}
--- a/src/input.c
+++ b/src/input.c
@@ -6,6 +6,7 @@
#include "backends/m8.h"
#include "common.h"
#include "render.h"
+#include "log_overlay.h"
#include <SDL3/SDL.h>
static unsigned char keyjazz_enabled = 0;
@@ -176,7 +177,7 @@
}
if (event->key.key == SDLK_RETURN && (event->key.mod & SDL_KMOD_ALT) > 0) {- toggle_fullscreen();
+ toggle_fullscreen(&ctx->conf);
return;
}
if (event->key.key == SDLK_F4 && (event->key.mod & SDL_KMOD_ALT) > 0) {@@ -184,13 +185,15 @@
return;
}
if (event->key.key == SDLK_ESCAPE) {- display_keyjazz_overlay(toggle_input_keyjazz(), keyjazz_base_octave, keyjazz_velocity);
+ if (ctx->app_state == RUN) {+ display_keyjazz_overlay(toggle_input_keyjazz(), keyjazz_base_octave, keyjazz_velocity);
+ }
return;
}
// Toggle in-app log overlay using config-defined key
if (event->key.scancode == ctx->conf.key_toggle_log) {- renderer_toggle_log_overlay();
+ log_overlay_toggle();
return;
}
--- /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_
+
--- a/src/main.c
+++ b/src/main.c
@@ -5,7 +5,7 @@
CFLAGS=-DDEBUG_MSG` */
// #define DEBUG_MSG
-#define APP_VERSION "v2.1.1"
+#define APP_VERSION "v2.2.0"
#include <SDL3/SDL.h>
#define SDL_MAIN_USE_CALLBACKS
@@ -19,13 +19,8 @@
#include "config.h"
#include "gamepads.h"
#include "render.h"
+#include "log_overlay.h"
-// On MacOS TARGET_OS_IOS is defined as 0, so make sure that it's consistent on other platforms as
-// well
-#ifndef TARGET_OS_IOS
-#define TARGET_OS_IOS 0
-#endif
-
static void do_wait_for_device(struct app_context *ctx) {static Uint64 ticks_poll_device = 0;
static int screensaver_initialized = 0;
@@ -99,10 +94,9 @@
if (TARGET_OS_IOS == 1) {// Predefined settings for iOS
conf.init_fullscreen = 1;
- } else {- // On other platforms, read config normally
- config_read(&conf);
}
+ config_read(&conf);
+
return conf;
}
@@ -151,7 +145,7 @@
char *config_filename = NULL;
// Initialize in-app log capture/overlay
- renderer_log_init();
+ log_overlay_init();
#ifndef NDEBUG
// Show debug messages in the application log
--- a/src/render.c
+++ b/src/render.c
@@ -9,13 +9,10 @@
#include "command.h"
#include "config.h"
#include "fx_cube.h"
+#include "log_overlay.h"
+#include "settings.h"
-#include "fonts/font1.h"
-#include "fonts/font2.h"
-#include "fonts/font3.h"
-#include "fonts/font4.h"
-#include "fonts/font5.h"
-#include "fonts/inline_font.h"
+#include "fonts/fonts.h"
#include <stdlib.h>
@@ -23,7 +20,6 @@
static SDL_Renderer *rend;
static SDL_Texture *main_texture;
static SDL_Texture *hd_texture = NULL;
-static SDL_Texture *log_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;
@@ -36,21 +32,8 @@
static int text_offset_y = 0;
static int waveform_max_height = 24;
-// Log overlay state
-static int log_overlay_visible = 0;
-static int log_overlay_needs_redraw = 0;
+// Log overlay moved to log_overlay.c
-// Log buffer configuration
-#define LOG_BUFFER_MAX_LINES 512
-#define LOG_LINE_MAX_CHARS 256
-static char log_lines[LOG_BUFFER_MAX_LINES][LOG_LINE_MAX_CHARS];
-static int log_line_start = 0; // index of the oldest line
-static int log_line_count = 0; // number of valid lines
-
-// Previous SDL log output forwarding
-static SDL_LogOutputFunction prev_log_output_fn = NULL;
-static void *prev_log_output_userdata = NULL;
-
static int texture_width = 320;
static int texture_height = 240;
static int hd_texture_width, hd_texture_height = 0;
@@ -57,9 +40,6 @@
static int screensaver_initialized = 0;
-struct inline_font *fonts[5] = {&font_v1_small, &font_v1_large, &font_v2_small, &font_v2_large,- &font_v2_huge};
-
uint8_t fullscreen = 0;
static uint8_t dirty = 0;
@@ -134,177 +114,31 @@
SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't create HD texture: %s", SDL_GetError());
}
- // Optionally, set a linear scaling mode for smoother rendering
+ // 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());
}
+ SDL_SetTextureBlendMode(hd_texture,SDL_BLENDMODE_BLEND);
+
setup_hd_texture_scaling();
}
-static void change_font(struct inline_font *font) {+static void change_font(const unsigned int index) {inline_font_close();
inline_font_set_renderer(rend);
- inline_font_initialize(font);
+ inline_font_initialize(fonts_get(index));
}
-// Append a formatted line to the circular log buffer
-static void log_buffer_append_line(const char *line) {- if (line[0] == '\0') {- return;
- }
- 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;
- }
- log_overlay_needs_redraw = 1;
-}
+// Log overlay API wrappers
+void renderer_log_init(void) { log_overlay_init(); }-// SDL log output function that mirrors to our in-app buffer in addition to the default handler
-static void sdl_log_capture(void *userdata, int category, SDL_LogPriority priority,
- const char *message) {- (void)userdata;
- (void)category;
- (void)priority;
-
- char formatted[LOG_LINE_MAX_CHARS];
- SDL_snprintf(formatted, sizeof(formatted), ">%s", message ? message : "");
-
- // Copy the formatted message into our buffer
- log_buffer_append_line(formatted);
-
- // Forward to the previous output function so messages still hit the console
- if (prev_log_output_fn != NULL) {- prev_log_output_fn(prev_log_output_userdata, category, priority, message);
- }
-}
-
-void renderer_log_init(void) {- // Preserve the existing output function and install our capture wrapper
- SDL_GetLogOutputFunction(&prev_log_output_fn, &prev_log_output_userdata);
- SDL_SetLogOutputFunction(sdl_log_capture, NULL);
-}
-
void renderer_toggle_log_overlay(void) {- log_overlay_visible = !log_overlay_visible;
- // Force redraw next present
+ log_overlay_toggle();
dirty = 1;
- log_overlay_needs_redraw = 1;
}
-// Render the log buffer into a texture for the overlay display
-static void render_log_overlay_texture(void) {- if (!log_overlay_visible) {- return;
- }
- if (log_texture == NULL) {- log_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
- texture_width, texture_height);
- if (log_texture == NULL) {- SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Couldn't create log texture: %s", SDL_GetError());
- return;
- }
- // Ensure overlay blends and scales consistently with the main texture
- SDL_SetTextureBlendMode(log_texture, SDL_BLENDMODE_BLEND);
- SDL_SetTextureScaleMode(log_texture, texture_scaling_mode);
- }
-
- if (!log_overlay_needs_redraw) {- return;
- }
- log_overlay_needs_redraw = 0;
-
- SDL_Texture *prev_target = SDL_GetRenderTarget(rend);
- SDL_SetRenderTarget(rend, log_texture);
-
- // Semi-transparent background rectangle
- SDL_SetRenderDrawColor(rend, 0, 0, 0, 220);
- SDL_RenderClear(rend);
-
- // Use a small font to fit more lines
- const int prev_font_mode = font_mode;
- inline_font_close();
- inline_font_initialize(fonts[0]);
-
- const int line_height = fonts[0]->glyph_y + 1;
- const int margin_x = 2;
- const int margin_y = 1;
- const int usable_width = texture_width - (margin_x * 2);
- const int cols = SDL_max(1, usable_width / (fonts[0]->glyph_x + 1));
-
- // Compute how many text rows fit
- const int max_rows = (texture_height - margin_y * 2) / line_height;
- int rows_needed = max_rows;
-
- // Determine start line and character offset so the overlay shows the most recent rows
- const int newest_idx =
- (log_line_start + log_line_count - 1 + LOG_BUFFER_MAX_LINES) % LOG_BUFFER_MAX_LINES;
- int start_idx = log_line_start;
- size_t start_char_offset = 0;
-
- if (log_line_count > 0) {- for (int n = 0; n < log_line_count && rows_needed > 0; n++) {- const int idx = (newest_idx - n + LOG_BUFFER_MAX_LINES) % LOG_BUFFER_MAX_LINES;
- const size_t len = SDL_strlen(log_lines[idx]);
- const int rows_for_line = SDL_max(1, (int)((len + cols - 1) / cols));
- if (rows_for_line >= rows_needed) {- int offset = (int)len - rows_needed * cols;
- if (offset < 0) {- offset = 0;
- }
- start_idx = idx;
- start_char_offset = (size_t)offset;
- break;
- }
- rows_needed -= rows_for_line;
- start_idx = idx;
- start_char_offset = 0;
- }
- }
-
- // Render forward from the computed start to the newest
- if (log_line_count > 0) {- int y = 1;
- int cur = start_idx;
- const int last = newest_idx;
- size_t offset = start_char_offset;
- while (1) {- const char *s = log_lines[cur];
- const size_t len = SDL_strlen(s);
- for (size_t pos = offset; pos < len && y < texture_height;) {- const Uint32 bg = 0xFFFFFF;
- const Uint32 fg = 0xFFFFFF;
- const size_t remaining = len - pos;
- size_t take = (size_t)cols < remaining ? (size_t)cols : remaining;
- char buf[LOG_LINE_MAX_CHARS];
- if (take >= sizeof(buf)) {- take = sizeof(buf) - 1;
- }
- SDL_memcpy(buf, s + pos, take);
- buf[take] = '\0';
- inprint(rend, buf, margin_x, y, fg, bg);
- y += line_height;
- pos += take;
- }
- if (cur == last || y >= texture_height) {- break;
- }
- cur = (cur + 1) % LOG_BUFFER_MAX_LINES;
- offset = 0;
- }
- }
-
- // Restore previous font
- inline_font_close();
- inline_font_initialize(fonts[prev_font_mode]);
-
- SDL_SetRenderTarget(rend, prev_target);
-}
-
static void check_and_adjust_window_and_texture_size(const int new_width, const int new_height) { if (texture_width == new_width && texture_height == new_height) {@@ -332,16 +166,16 @@
SDL_DestroyTexture(main_texture);
}
- // Drop log texture so it can be recreated with the correct size
- if (log_texture != NULL) {- SDL_DestroyTexture(log_texture);
- log_texture = NULL;
- }
+ // Notify log overlay to drop its cached texture so it can be recreated with the new size
+ log_overlay_invalidate();
main_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
texture_width, texture_height);
SDL_SetTextureScaleMode(main_texture, texture_scaling_mode);
SDL_SetRenderTarget(rend, main_texture);
+
+ // Notify settings overlay about logical render size change so it can recreate its cache
+ settings_on_texture_size_change(rend);
}
// Set the M8 hardware model in use. 0 = MK1, 1 = MK2
@@ -368,11 +202,12 @@
return;
font_mode = mode;
- screen_offset_y = fonts[mode]->screen_offset_y;
- text_offset_y = fonts[mode]->text_offset_y;
- waveform_max_height = fonts[mode]->waveform_max_height;
+ const struct inline_font *new_font = fonts_get(mode);
+ screen_offset_y = new_font->screen_offset_y;
+ text_offset_y = new_font->text_offset_y;
+ waveform_max_height = new_font->waveform_max_height;
- change_font(fonts[mode]);
+ change_font(mode);
SDL_LogDebug(SDL_LOG_CATEGORY_RENDER, "Font mode %i, Screen offset %i", mode, screen_offset_y);
}
@@ -385,18 +220,16 @@
if (hd_texture != NULL) {SDL_DestroyTexture(hd_texture);
}
- if (log_texture != NULL) {- SDL_DestroyTexture(log_texture);
- }
+ log_overlay_destroy();
SDL_DestroyRenderer(rend);
SDL_DestroyWindow(win);
}
-void toggle_fullscreen(void) {+int toggle_fullscreen(config_params_s *conf) {const unsigned long fullscreen_state = SDL_GetWindowFlags(win) & SDL_WINDOW_FULLSCREEN;
-
SDL_SetWindowFullscreen(win, fullscreen_state ? false : true);
+ conf->init_fullscreen = (unsigned int)!fullscreen_state;
SDL_SyncWindow(win);
if (fullscreen_state) {// Show cursor when in a windowed state
@@ -406,6 +239,7 @@
}
dirty = 1;
+ return (int)conf->init_fullscreen;
}
int draw_character(struct draw_character_command *command) {@@ -518,8 +352,8 @@
void display_keyjazz_overlay(const uint8_t show, const uint8_t base_octave,
const uint8_t velocity) {- const Uint16 overlay_offset_x = texture_width - (fonts[font_mode]->glyph_x * 7 + 1);
- const Uint16 overlay_offset_y = texture_height - (fonts[font_mode]->glyph_y + 1);
+ const Uint16 overlay_offset_x = texture_width - (fonts_get(font_mode)->glyph_x * 7 + 1);
+ const Uint16 overlay_offset_y = texture_height - (fonts_get(font_mode)->glyph_y + 1);
const Uint32 bg_color =
global_background_color.r << 16 | global_background_color.g << 8 | global_background_color.b;
@@ -527,7 +361,7 @@
char overlay_text[7];
SDL_snprintf(overlay_text, sizeof(overlay_text), "%02X %u", velocity, base_octave);
inprint(rend, overlay_text, overlay_offset_x, overlay_offset_y, 0xC8C8C8, bg_color);
- inprint(rend, "*", overlay_offset_x + (fonts[font_mode]->glyph_x * 5 + 5), overlay_offset_y,
+ inprint(rend, "*", overlay_offset_x + (fonts_get(font_mode)->glyph_x * 5 + 5), overlay_offset_y,
0xFF0000, bg_color);
} else {inprint(rend, " ", overlay_offset_x, overlay_offset_y, 0xC8C8C8, bg_color);
@@ -615,8 +449,8 @@
}
void render_screen(config_params_s *conf) {- if (!dirty) {- // No draw commands have been issued since the last function call, do nothing
+ if (!dirty && !settings_is_open()) {+ // No draw commands and settings overlay not active, skip rendering
return;
}
@@ -642,6 +476,17 @@
}
if (conf->integer_scaling) {+ SDL_SetRenderTarget(rend, main_texture);
+ // Render log overlay (composites if visible)
+ log_overlay_render(rend, texture_width, texture_height, texture_scaling_mode, font_mode);
+
+ // Settings overlay composited last
+ if (settings_is_open()) {+ settings_render_overlay(rend, conf, texture_width, texture_height);
+ }
+
+ SDL_SetRenderTarget(rend, NULL);
+
// 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());
@@ -679,8 +524,16 @@
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());
- };
+ }
+ // Render log overlay (composites if visible)
+ log_overlay_render(rend, texture_width, texture_height, texture_scaling_mode, font_mode);
+
+ // Settings overlay composited last
+ if (settings_is_open()) {+ settings_render_overlay(rend, conf, texture_width, texture_height);
+ }
+
// 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",
@@ -713,14 +566,6 @@
}
}
- // Ensure log overlay is up to date, then composite it if visible before present
- if (log_overlay_visible) {- render_log_overlay_texture();
- if (log_texture) {- SDL_RenderTexture(rend, log_texture, NULL, NULL);
- }
- }
-
if (!SDL_RenderPresent(rend)) {SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't present renderer: %s", SDL_GetError());
}
@@ -743,7 +588,7 @@
global_background_color.g = 0;
global_background_color.b = 0;
fx_cube_init(rend, (SDL_Color){255, 255, 255, 255}, texture_width, texture_height,- fonts[font_mode]->glyph_x);
+ fonts_get(font_mode)->glyph_x);
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Screensaver initialized");
screensaver_initialized = 1;
return 1;
--- a/src/render.h
+++ b/src/render.h
@@ -22,14 +22,10 @@
void set_m8_model(unsigned int model);
void render_screen(config_params_s *conf);
-void toggle_fullscreen(void);
+int toggle_fullscreen(config_params_s *conf);
void display_keyjazz_overlay(uint8_t show, uint8_t base_octave, uint8_t velocity);
void show_error_message(const char *message);
-
-// Log overlay controls
-void renderer_log_init(void);
-void renderer_toggle_log_overlay(void);
int screensaver_init(void);
void screensaver_draw(void);
--- /dev/null
+++ b/src/settings.c
@@ -1,0 +1,668 @@
+#include "settings.h"
+
+#include "SDL2_inprint.h"
+#include "backends/audio.h"
+#include "common.h"
+#include "render.h"
+
+#include "fonts/fonts.h"
+#include <SDL3/SDL.h>
+
+// Internal state
+static int g_settings_open = 0;
+static int g_selected_index = 0;
+static int g_needs_redraw = 1;
+static int g_config_saved = 0;
+static SDL_Texture *g_settings_texture = NULL;
+
+
+typedef enum capture_mode_t {+ CAPTURE_NONE,
+ CAPTURE_KEY,
+ CAPTURE_BUTTON,
+ CAPTURE_AXIS
+} capture_mode_t;
+
+static capture_mode_t g_capture_mode = CAPTURE_NONE;
+static void *g_capture_target = NULL; // points to conf field
+
+typedef enum item_type_t {+ ITEM_HEADER,
+ ITEM_TOGGLE_BOOL,
+ ITEM_SAVE,
+ ITEM_CLOSE,
+ ITEM_SUBMENU,
+ ITEM_BIND_KEY,
+ ITEM_BIND_BUTTON,
+ ITEM_BIND_AXIS,
+ ITEM_INT_ADJ
+} item_type_t;
+
+typedef struct setting_item_s {+ const char *label;
+ item_type_t type;
+ void *target;
+ int step;
+ int min_val;
+ int max_val;
+} setting_item_s;
+
+typedef enum settings_view_t { VIEW_ROOT, VIEW_KEYS, VIEW_GAMEPAD, VIEW_ANALOG } settings_view_t;+static settings_view_t g_view = VIEW_ROOT;
+extern SDL_Gamepad *game_controllers[4];
+
+static void add_item(setting_item_s *items, int *count, const char *label, item_type_t type,
+ void *target, int step, int min_val, int max_val) {+ items[*count] = (setting_item_s){label, type, target, step, min_val, max_val};+ (*count)++;
+}
+
+static void build_menu(const config_params_s *conf, setting_item_s *items, int *count) {+ *count = 0;
+ switch (g_view) {+ case VIEW_ROOT:
+ add_item(items, count, "Graphics ", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Integer scaling", ITEM_TOGGLE_BOOL, (void *)&conf->integer_scaling, 0,
+ 0, 0);
+ // SDL apps are always full screen on iOS, hide the option
+ if (TARGET_OS_IOS == 0) {+ add_item(items, count, "Fullscreen ", ITEM_TOGGLE_BOOL, (void *)&conf->init_fullscreen, 0,
+ 0, 0);
+ }
+ add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
+ // Audio routing does not work on iOS, hide the items when building for that
+ if (TARGET_OS_IOS == 0) {+ add_item(items, count, "Audio ", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Audio enabled ", ITEM_TOGGLE_BOOL, (void *)&conf->audio_enabled, 0, 0,
+ 0);
+ add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
+ }
+ add_item(items, count, "Bindings ", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Keyboard bindings >", ITEM_SUBMENU, NULL, 0, 0, 0);
+ add_item(items, count, "Gamepad bindings >", ITEM_SUBMENU, NULL, 0, 0, 0);
+ add_item(items, count, "Gamepad analog axis >", ITEM_SUBMENU, NULL, 0, 0, 0);
+ add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Save", ITEM_SAVE, NULL, 0, 0, 0);
+ add_item(items, count, "Close", ITEM_CLOSE, NULL, 0, 0, 0);
+ break;
+ case VIEW_KEYS:
+ add_item(items, count, "Keyboard bindings", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Up ", ITEM_BIND_KEY, (void *)&conf->key_up, 0, 0, 0);
+ add_item(items, count, "Left ", ITEM_BIND_KEY, (void *)&conf->key_left, 0, 0, 0);
+ add_item(items, count, "Down ", ITEM_BIND_KEY, (void *)&conf->key_down, 0, 0, 0);
+ add_item(items, count, "Right ", ITEM_BIND_KEY, (void *)&conf->key_right, 0, 0, 0);
+ add_item(items, count, "Select ", ITEM_BIND_KEY, (void *)&conf->key_select, 0, 0, 0);
+ add_item(items, count, "Start ", ITEM_BIND_KEY, (void *)&conf->key_start, 0, 0, 0);
+ add_item(items, count, "Opt ", ITEM_BIND_KEY, (void *)&conf->key_opt, 0, 0, 0);
+ add_item(items, count, "Edit ", ITEM_BIND_KEY, (void *)&conf->key_edit, 0, 0, 0);
+ add_item(items, count, "Delete ", ITEM_BIND_KEY, (void *)&conf->key_delete, 0, 0, 0);
+ add_item(items, count, "Reset ", ITEM_BIND_KEY, (void *)&conf->key_reset, 0, 0, 0);
+ add_item(items, count, "Toggle audio", ITEM_BIND_KEY, (void *)&conf->key_toggle_audio, 0, 0, 0);
+ add_item(items, count, "Toggle log ", ITEM_BIND_KEY, (void *)&conf->key_toggle_log, 0, 0, 0);
+ add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Back", ITEM_CLOSE, NULL, 0, 0, 0);
+ break;
+ case VIEW_GAMEPAD:
+ add_item(items, count, "Gamepad bindings", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Up ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_up, 0, 0, 0);
+ add_item(items, count, "Left ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_left, 0, 0, 0);
+ add_item(items, count, "Down ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_down, 0, 0, 0);
+ add_item(items, count, "Right ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_right, 0, 0, 0);
+ add_item(items, count, "Select", ITEM_BIND_BUTTON, (void *)&conf->gamepad_select, 0, 0, 0);
+ add_item(items, count, "Start ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_start, 0, 0, 0);
+ add_item(items, count, "Opt ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_opt, 0, 0, 0);
+ add_item(items, count, "Edit ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_edit, 0, 0, 0);
+ add_item(items, count, "Quit ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_quit, 0, 0, 0);
+ add_item(items, count, "Reset ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_reset, 0, 0, 0);
+ add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Back", ITEM_CLOSE, NULL, 0, 0, 0);
+ break;
+ case VIEW_ANALOG:
+ add_item(items, count, "Gamepad analog axis", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Deadzone", ITEM_INT_ADJ, (void *)&conf->gamepad_analog_threshold, 1000,
+ 1000, 32767);
+ add_item(items, count, "Axis Up/Down ", ITEM_BIND_AXIS,
+ (void *)&conf->gamepad_analog_axis_updown, 0, 0, 0);
+ add_item(items, count, "Axis Left/Right", ITEM_BIND_AXIS,
+ (void *)&conf->gamepad_analog_axis_leftright, 0, 0, 0);
+ add_item(items, count, "Axis Select ", ITEM_BIND_AXIS,
+ (void *)&conf->gamepad_analog_axis_select, 0, 0, 0);
+ add_item(items, count, "Axis Start ", ITEM_BIND_AXIS,
+ (void *)&conf->gamepad_analog_axis_start, 0, 0, 0);
+ add_item(items, count, "Axis Opt ", ITEM_BIND_AXIS,
+ (void *)&conf->gamepad_analog_axis_opt, 0, 0, 0);
+ add_item(items, count, "Axis Edit ", ITEM_BIND_AXIS,
+ (void *)&conf->gamepad_analog_axis_edit, 0, 0, 0);
+ add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
+ add_item(items, count, "Back", ITEM_CLOSE, NULL, 0, 0, 0);
+ break;
+ }
+}
+
+static void settings_destroy_texture(SDL_Renderer *rend) {+ (void)rend;
+ if (g_settings_texture != NULL) {+ SDL_DestroyTexture(g_settings_texture);
+ g_settings_texture = NULL;
+ }
+}
+
+void settings_toggle_open(void) {+ g_settings_open = !g_settings_open;
+ g_selected_index = 1; // first actionable item
+ g_capture_mode = CAPTURE_NONE;
+ g_capture_target = NULL;
+ g_needs_redraw = 1;
+}
+
+bool settings_is_open(void) { return g_settings_open != 0; }+
+static void settings_move(const config_params_s *conf, int delta) {+ if (!g_settings_open)
+ return;
+
+ setting_item_s items[64];
+ int count = 0;
+ build_menu(conf, items, &count);
+
+ int idx = g_selected_index + delta;
+
+ // Clamp within bounds
+ if (idx < 0)
+ idx = 0;
+ if (idx >= count)
+ idx = count - 1;
+
+ // Skip headers in the direction of movement
+ const int step = (delta >= 0) ? 1 : -1;
+ while (idx >= 0 && idx < count && items[idx].type == ITEM_HEADER) {+ idx += step;
+ }
+
+ // Ensure we don't select before first actionable item
+ if (idx < 1)
+ idx = 1;
+
+ // Final safety clamp
+ if (idx >= count)
+ idx = count - 1;
+
+ g_selected_index = idx;
+ g_needs_redraw = 1;
+}
+
+static void settings_activate(struct app_context *ctx, const setting_item_s *items, int count) {+ if (g_selected_index < 1 || g_selected_index >= count)
+ return;
+ const setting_item_s *it = &items[g_selected_index];
+ config_params_s *conf = &ctx->conf;
+
+ switch (it->type) {+ case ITEM_TOGGLE_BOOL: {+ unsigned int *val = it->target;
+ *val = *val ? 0 : 1;
+ if (it->target == &conf->init_fullscreen) {+ toggle_fullscreen(conf);
+ }
+ if (it->target == &conf->integer_scaling) {+ renderer_fix_texture_scaling_after_window_resize(conf);
+ }
+ if (it->target == &conf->audio_enabled && ctx->device_connected) {+ audio_toggle(ctx->conf.audio_device_name, ctx->conf.audio_buffer_size);
+ }
+ g_needs_redraw = 1;
+ break;
+ }
+ case ITEM_SAVE: {+ write_config(conf);
+ g_needs_redraw = 1;
+ g_config_saved = 1;
+ break;
+ }
+ case ITEM_CLOSE:
+ settings_toggle_open();
+ break;
+ case ITEM_BIND_KEY:
+ g_capture_mode = CAPTURE_KEY;
+ g_capture_target = it->target;
+ g_needs_redraw = 1;
+ break;
+ case ITEM_BIND_BUTTON:
+ g_capture_mode = CAPTURE_BUTTON;
+ g_capture_target = it->target;
+ g_needs_redraw = 1;
+ break;
+ case ITEM_BIND_AXIS:
+ g_capture_mode = CAPTURE_AXIS;
+ g_capture_target = it->target;
+ g_needs_redraw = 1;
+ break;
+ case ITEM_INT_ADJ:
+ case ITEM_HEADER:
+ break;
+ default:
+ break;
+ }
+}
+
+void settings_handle_event(struct app_context *ctx, const SDL_Event *e) {+ if (!g_settings_open)
+ return;
+
+ if (e->type == SDL_EVENT_KEY_DOWN) {+ if (e->key.key == SDLK_ESCAPE || e->key.key == SDLK_F1) {+ if (g_capture_mode != CAPTURE_NONE) {+ g_capture_mode = CAPTURE_NONE;
+ g_capture_target = NULL;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (g_view != VIEW_ROOT) {+ g_view = VIEW_ROOT;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ settings_toggle_open();
+ return;
+ }
+ // Capture key remap
+ if (g_capture_mode == CAPTURE_KEY) {+ if (g_capture_target != NULL) {+ unsigned int *dst = g_capture_target;
+ *dst = e->key.scancode;
+ }
+ g_capture_mode = CAPTURE_NONE;
+ g_capture_target = NULL;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (e->key.key == SDLK_UP) {+ settings_move(&ctx->conf, -1);
+ return;
+ }
+ if (e->key.key == SDLK_DOWN) {+ settings_move(&ctx->conf, 1);
+ return;
+ }
+ if (e->key.key == SDLK_LEFT || e->key.key == SDLK_RIGHT) {+ setting_item_s items[64];
+ int count = 0;
+ build_menu(&ctx->conf, items, &count);
+ if (g_selected_index > 0 && g_selected_index < count) {+ setting_item_s *it = &items[g_selected_index];
+ if (it->type == ITEM_INT_ADJ && it->target != NULL) {+ int *val = (int *)it->target;
+ int delta = (e->key.key == SDLK_LEFT ? -it->step : it->step);
+ int newv = *val + delta;
+ if (newv < it->min_val)
+ newv = it->min_val;
+ if (newv > it->max_val)
+ newv = it->max_val;
+ *val = newv;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+ }
+ if (e->key.key == SDLK_RETURN || e->key.key == SDLK_SPACE) {+ setting_item_s items[64];
+ int count = 0;
+ build_menu(&ctx->conf, items, &count);
+ // Handle entering submenus from root based on item type
+ if (g_view == VIEW_ROOT) {+ const setting_item_s *it = &items[g_selected_index];
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Keyboard bindings") == it->label) {+ g_view = VIEW_KEYS;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Gamepad bindings") == it->label) {+ g_view = VIEW_GAMEPAD;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Gamepad analog axis") == it->label) {+ g_view = VIEW_ANALOG;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+ // Back item in submenus
+ if (g_view != VIEW_ROOT) {+ const setting_item_s *it = &items[g_selected_index];
+ if (it->type == ITEM_CLOSE && it->label && SDL_strcmp(it->label, "Back") == 0) {+ g_view = VIEW_ROOT;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+ settings_activate(ctx, items, count);
+ return;
+ }
+ }
+
+ // Gamepad navigation and cancel/back handling
+ if (e->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {+ SDL_GamepadButton btn = e->gbutton.button;
+
+ // Cancel capture or go back/close with B/Back
+ if (btn == SDL_GAMEPAD_BUTTON_EAST || btn == SDL_GAMEPAD_BUTTON_BACK) {+ if (g_capture_mode != CAPTURE_NONE) {+ g_capture_mode = CAPTURE_NONE;
+ g_capture_target = NULL;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (g_view != VIEW_ROOT) {+ g_view = VIEW_ROOT;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ settings_toggle_open();
+ return;
+ }
+
+ // If capturing a button, let the capture handler below process it
+ if (g_capture_mode == CAPTURE_NONE) {+ // D-pad navigation
+ if (btn == SDL_GAMEPAD_BUTTON_DPAD_UP) {+ settings_move(&ctx->conf, -1);
+ return;
+ }
+ if (btn == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {+ settings_move(&ctx->conf, 1);
+ return;
+ }
+ if (btn == SDL_GAMEPAD_BUTTON_DPAD_LEFT || btn == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {+ setting_item_s items[64];
+ int count = 0;
+ build_menu(&ctx->conf, items, &count);
+ if (g_selected_index > 0 && g_selected_index < count) {+ setting_item_s *it = &items[g_selected_index];
+ if (it->type == ITEM_INT_ADJ && it->target != NULL) {+ int *val = (int *)it->target;
+ int delta = (btn == SDL_GAMEPAD_BUTTON_DPAD_LEFT ? -it->step : it->step);
+ int newv = *val + delta;
+ if (newv < it->min_val)
+ newv = it->min_val;
+ if (newv > it->max_val)
+ newv = it->max_val;
+ *val = newv;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+ }
+ // Activate/select with A
+ if (btn == SDL_GAMEPAD_BUTTON_SOUTH) {+ setting_item_s items[64];
+ int count = 0;
+ build_menu(&ctx->conf, items, &count);
+ // Handle entering submenus from root based on item type
+ if (g_view == VIEW_ROOT) {+ const setting_item_s *it = &items[g_selected_index];
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Keyboard bindings") == it->label) {+ g_view = VIEW_KEYS;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Gamepad bindings") == it->label) {+ g_view = VIEW_GAMEPAD;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Gamepad analog axis") == it->label) {+ g_view = VIEW_ANALOG;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+ // Back item in submenus
+ if (g_view != VIEW_ROOT) {+ const setting_item_s *it = &items[g_selected_index];
+ if (it->type == ITEM_CLOSE && it->label && SDL_strcmp(it->label, "Back") == 0) {+ g_view = VIEW_ROOT;
+ g_selected_index = 1;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+ settings_activate(ctx, items, count);
+ return;
+ }
+ }
+ }
+
+ // Capture gamepad button
+ if (g_capture_mode == CAPTURE_BUTTON && e->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {+ if (g_capture_target != NULL) {+ int *dst = (int *)g_capture_target;
+ *dst = e->gbutton.button;
+ }
+ g_capture_mode = CAPTURE_NONE;
+ g_capture_target = NULL;
+ g_needs_redraw = 1;
+ return;
+ }
+ // Capture axis on significant motion
+ if (g_capture_mode == CAPTURE_AXIS && e->type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {+ if (SDL_abs(e->gaxis.value) > 16000) {+ if (g_capture_target != NULL) {+ int *dst = (int *)g_capture_target;
+ *dst = e->gaxis.axis;
+ }
+ g_capture_mode = CAPTURE_NONE;
+ g_capture_target = NULL;
+ g_needs_redraw = 1;
+ return;
+ }
+ }
+}
+
+void settings_render_overlay(SDL_Renderer *rend, const config_params_s *conf, int texture_w,
+ int texture_h) {+ if (!g_settings_open)
+ return;
+
+ const struct inline_font *previous_font = inline_font_get_current();
+ if (previous_font->glyph_x != fonts_get(0)->glyph_x) {+ // Switch to small font if not active already
+ inline_font_close();
+ inline_font_initialize(fonts_get(0));
+ }
+
+ if (g_settings_texture == NULL) {+ g_settings_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
+ texture_w, texture_h);
+ if (g_settings_texture == NULL) {+ inline_font_close();
+ inline_font_initialize(previous_font);
+ return;
+ }
+ SDL_SetTextureBlendMode(g_settings_texture, SDL_BLENDMODE_BLEND);
+ SDL_SetTextureScaleMode(g_settings_texture, SDL_SCALEMODE_NEAREST);
+ g_needs_redraw = 1;
+ }
+
+ if (!g_needs_redraw)
+ goto composite;
+ g_needs_redraw = 0;
+
+ SDL_Texture *prev = SDL_GetRenderTarget(rend);
+ SDL_SetRenderTarget(rend, g_settings_texture);
+
+ // Background
+ SDL_SetRenderDrawColor(rend, 0, 0, 0, 240);
+ SDL_RenderClear(rend);
+
+ // Title and items
+ const Uint32 fg = 0xFFFFFF;
+ const Uint32 bg = 0xFFFFFF;
+ const Uint32 selected_item_fg = 0x00FFFF;
+ const Uint32 selected_item_bg = 0x00FFFF;
+ const Uint32 title = 0xFF0000;
+ const Uint32 section_header = 0xAAAAFF;
+ const int margin_x_unselected = fonts_get(0)->glyph_x+1;
+ const int margin_x_selected = fonts_get(0)->glyph_x+1;
+ int x = 8;
+ int y = 8;
+
+ inprint(rend, "M8C Config", x, y, title, title);
+ y += 24;
+
+ setting_item_s items[64];
+ int count = 0;
+ build_menu(conf, items, &count);
+ if (g_selected_index >= count)
+ g_selected_index = count - 1;
+
+ if (g_capture_mode == CAPTURE_KEY) {+ inprint(rend, "Press a key... (Esc to cancel)", x, y, selected_item_fg, selected_item_fg);
+ } else if (g_capture_mode == CAPTURE_BUTTON) {+ inprint(rend, "Press a gamepad button...", x, y, selected_item_fg, selected_item_fg);
+ } else if (g_capture_mode == CAPTURE_AXIS) {+ inprint(rend, "Move a gamepad axis...", x, y, selected_item_fg, selected_item_fg);
+ }
+ if (g_config_saved) {+ inprint(rend, "Configuration saved", x, y, selected_item_fg, selected_item_fg);
+ g_config_saved = 0;
+ }
+ y += 12;
+
+
+ for (int i = 0; i < count; i++) {+ const int selected = (i == g_selected_index);
+ const setting_item_s *it = &items[i];
+ if (it->type == ITEM_HEADER) {+ inprint(rend, it->label, x, y, section_header, section_header);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_TOGGLE_BOOL) {+ const unsigned int *val = (const unsigned int *)it->target;
+ char line[160];
+ SDL_snprintf(line, sizeof line, "%s [%s]", it->label, (*val ? "On" : "Off"));
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_BIND_KEY) {+ int sc = *(int *)it->target;
+ const char *name = SDL_GetScancodeName((SDL_Scancode)sc);
+ if (name == NULL || name[0] == '\0') {+ name = "Unknown";
+ }
+ char line[160];
+ SDL_snprintf(line, sizeof line, "%s: %s", it->label, name);
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_BIND_BUTTON) {+ int v = *(int *)it->target;
+ const char *name = NULL;
+
+ // Pick the first active controller if available
+ SDL_Gamepad *pad = NULL;
+ for (int gi = 0; gi < 4; gi++) {+ if (game_controllers[gi] != NULL) {+ pad = game_controllers[gi];
+ break;
+ }
+ }
+
+ if (pad) {+ // Use controller-specific face button labels when possible
+ SDL_GamepadButtonLabel lbl = SDL_GetGamepadButtonLabel(pad, (SDL_GamepadButton)v);
+ if (lbl != SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN) {+ name = SDL_GetGamepadStringForButton(v);
+ }
+ }
+ // Generic fallback by standardized button name
+ if (name == NULL || name[0] == '\0') {+ name = SDL_GetGamepadStringForButton((SDL_GamepadButton)v);
+ }
+
+ char line[160];
+ if (name && name[0] != '\0') {+ SDL_snprintf(line, sizeof line, "%s: %s", it->label, name);
+ } else {+ SDL_snprintf(line, sizeof line, "%s: %d", it->label, v);
+ }
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_BIND_AXIS) {+ int v = *(int *)it->target;
+ const char *name = SDL_GetGamepadStringForAxis((SDL_GamepadAxis)v);
+
+ char line[160];
+ if (name && name[0] != '\0') {+ SDL_snprintf(line, sizeof line, "%s: %s", it->label, name);
+ } else {+ SDL_snprintf(line, sizeof line, "%s: %d", it->label, v);
+ }
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_INT_ADJ) {+ char line[160];
+ SDL_snprintf(line, sizeof line, "%s: %d (Left/Right)", it->label, *(int *)it->target);
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_SAVE || it->type == ITEM_CLOSE) {+ char line[160];
+ SDL_snprintf(line, sizeof line, "%s", it->label);
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ continue;
+ }
+ if (it->type == ITEM_SUBMENU) {+ char line[160];
+ SDL_snprintf(line, sizeof line, "%s", it->label);
+ inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
+ selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
+ y += 12;
+ }
+ }
+
+ SDL_SetRenderTarget(rend, prev);
+
+composite:
+ SDL_RenderTexture(rend, g_settings_texture, NULL, NULL);
+ if (previous_font->glyph_x != fonts_get(0)->glyph_x) {+ inline_font_close();
+ inline_font_initialize(previous_font);
+ }
+}
+
+// Handle renderer/size resets: drop the cached texture to recreate at new size on next frame
+void settings_on_texture_size_change(SDL_Renderer *rend) {+ settings_destroy_texture(rend);
+ g_needs_redraw = 1;
+}
--- /dev/null
+++ b/src/settings.h
@@ -1,0 +1,35 @@
+// Simple in-app settings overlay for configuring input bindings and a few options
+
+#ifndef SETTINGS_H_
+#define SETTINGS_H_
+
+#include <SDL3/SDL.h>
+#include <stdbool.h>
+
+#include "config.h"
+
+// Forward declaration to avoid header coupling
+struct app_context;
+
+// Open/close and state query
+void settings_toggle_open(void);
+bool settings_is_open(void);
+
+// Event handling (consume SDL events when open)
+void settings_handle_event(struct app_context *ctx, const SDL_Event *e);
+
+// Render the settings overlay into a texture-sized canvas and composite to the window
+// texture_w/texture_h should be the logical render size (e.g. 320x240 or 480x320)
+void settings_render_overlay(SDL_Renderer *rend, const config_params_s *conf, int texture_w, int texture_h);
+
+// Notify settings overlay that logical render size changed; drops cached texture
+void settings_on_texture_size_change(SDL_Renderer *rend);
+
+// Set the font (SDL_Texture*) to be used while rendering the settings menu.
+// Pass NULL to use the current global font.
+void settings_set_font(SDL_Texture *font);
+
+
+#endif // SETTINGS_H_
+
+
--
⑨