From 8d7797898ecbfad8722760402941eee2a35420c9 Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:51:47 +0300 Subject: [PATCH 1/8] replace generic "A" drive activity with amber LED like a real STFM --- libretro/hatari-mapper.c | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/libretro/hatari-mapper.c b/libretro/hatari-mapper.c index 9dbfa899..e7840f36 100644 --- a/libretro/hatari-mapper.c +++ b/libretro/hatari-mapper.c @@ -1156,30 +1156,56 @@ void update_input(void) if (hatari_led_status_display) { + /* Draw Atari STFM-style amber LED rectangle directly into framebuffer. + Thin horizontal bar, warm amber colour (RGB565 0xFEC0 ~ #FFD800). + Position: top-right, just below the RetroArch FPS counter line. + FPS text sits at roughly y=0..14; we place the LED at y=18. + LEDs stacked right-to-left from the right edge, 20px apart. */ + #define LED_COLOR 0xFEC0 /* warm amber, matches real STFM hardware */ + #define LED_W 6 /* half-width -> full bar = 15px */ + #define LED_H 2 /* half-height -> full bar = 5px */ + #define LED_CY 8 /* just below FPS text */ + + extern int retrow; + if (LEDA) { + int cx = retrow - 12, x, y; + for (y = LED_CY - LED_H; y <= LED_CY + LED_H; y++) + for (x = cx - LED_W; x <= cx + LED_W; x++) + bmp[y * retrow + x] = LED_COLOR; + if (status_update_joymouse || status_update_mousespeed) strcat(msg, " || A"); - else - retro_status(60, "A"); } if (LEDB) { + int cx = retrow - 24, x, y; + for (y = LED_CY - LED_H; y <= LED_CY + LED_H; y++) + for (x = cx - LED_W; x <= cx + LED_W; x++) + bmp[y * retrow + x] = LED_COLOR; + if (status_update_joymouse || status_update_mousespeed) strcat(msg, " || B"); - else - retro_status(60, "B"); } if (LEDC) { + int cx = retrow - 36, x, y; + for (y = LED_CY - LED_H; y <= LED_CY + LED_H; y++) + for (x = cx - LED_W; x <= cx + LED_W; x++) + bmp[y * retrow + x] = LED_COLOR; + if (status_update_joymouse || status_update_mousespeed) strcat(msg, " || C"); - else - retro_status(60, "C"); if (STATUTON == -1) LEDC = 0; } + + #undef LED_COLOR + #undef LED_W + #undef LED_H + #undef LED_CY } if ( msg[0] ) From 70027447d3290a2628bfe79119e10b8863ed52ee Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:05:39 +0300 Subject: [PATCH 2/8] add floppy sound --- libretro/floppy_sound.c | 228 ++++++++++++++++++++++++++++++++++++++++ libretro/floppy_sound.h | 22 ++++ 2 files changed, 250 insertions(+) create mode 100644 libretro/floppy_sound.c create mode 100644 libretro/floppy_sound.h diff --git a/libretro/floppy_sound.c b/libretro/floppy_sound.c new file mode 100644 index 00000000..aed4196d --- /dev/null +++ b/libretro/floppy_sound.c @@ -0,0 +1,228 @@ +/* + * floppy_sound.c - Atari ST floppy drive sounds for Hatari libretro + * + * Plays a click/seek sound whenever a floppy LED turns on (rising edge). + * Supports up to 2 simultaneous drives (A and B). + * + * External sample: place a raw PCM file at: + * /floppy.raw + * Format: signed 16-bit little-endian stereo, 44100 Hz, no header + * Convert with: ffmpeg -i click.wav -f s16le -ar 44100 -ac 2 floppy.raw + * + * If no file is found, a built-in synthetic click is used automatically. + */ + +#include +#include +#include +#include +#include + +#include "floppy_sound.h" + +/* ------------------------------------------------------------------ */ +/* Built-in synthetic click */ +/* ~12ms of a decaying square wave at ~1kHz, 44100Hz stereo int16 */ +/* ------------------------------------------------------------------ */ +#define BUILTIN_SAMPLES 2425 /* ~55ms @ 44100Hz */ +static int16_t builtin_pcm[BUILTIN_SAMPLES * 2]; +static int builtin_generated = 0; + +static void generate_builtin(void) +{ + int i; + /* Total ~55ms @ 44100Hz = 2425 samples + * Layers: + * 1. Initial mechanical THUD - low thump ~120Hz, sharp attack, fast decay (~15ms) + * 2. Stepper motor BUZZ - mid freq ~800Hz, short burst (~20ms) + * 3. Head seek CHIRP - descending ~2400->800Hz sweep (~25ms) + * 4. Light chassis RATTLE - broadband noise burst (~10ms, offset 5ms) + * All summed and normalised to int16 range. + */ + for (i = 0; i < BUILTIN_SAMPLES; i++) + { + float t = (float)i / 44100.0f; /* time in seconds */ + float s = 0.0f; + + /* --- 1. mechanical thud: 120 Hz, decays in ~12ms --- */ + { + float env = expf(-t / 0.012f); + s += sinf(2.0f * 3.14159265f * 120.0f * t) * env * 0.55f; + } + + /* --- 2. stepper buzz: 800 Hz, onset 2ms, decays by 20ms --- */ + { + float onset = (t > 0.002f) ? 1.0f : (t / 0.002f); + float env = onset * expf(-t / 0.008f); + /* slight frequency wobble to sound mechanical */ + float freq = 800.0f + 60.0f * sinf(2.0f * 3.14159265f * 180.0f * t); + s += sinf(2.0f * 3.14159265f * freq * t) * env * 0.30f; + } + + /* --- 3. head seek chirp: 2400->700 Hz sweep over 25ms --- */ + { + float chirp_len = 0.025f; + float env = (t < chirp_len) + ? expf(-t / 0.018f) + : 0.0f; + float freq = 2400.0f - (2400.0f - 700.0f) * (t / chirp_len); + if (t < chirp_len) + s += sinf(2.0f * 3.14159265f * freq * t) * env * 0.20f; + } + + /* --- 4. broadband rattle: pseudo-noise via fast square LFO, 5-15ms --- */ + { + float t2 = t - 0.005f; + if (t2 > 0.0f && t2 < 0.012f) + { + float env = expf(-t2 / 0.005f); + /* cheap noise: XOR-ish pattern using incommensurable freqs */ + float n = sinf(2.0f * 3.14159265f * 3700.0f * t2) + * sinf(2.0f * 3.14159265f * 2300.0f * t2) + * sinf(2.0f * 3.14159265f * 970.0f * t2); + s += n * env * 0.18f; + } + } + + /* clamp and write stereo */ + if (s > 1.0f) s = 1.0f; + if (s < -1.0f) s = -1.0f; + int16_t out = (int16_t)(s * 28000.0f); + builtin_pcm[i * 2 ] = out; + builtin_pcm[i * 2 + 1] = out; + } + builtin_generated = 1; +} + +/* ------------------------------------------------------------------ */ +/* Sample storage */ +/* ------------------------------------------------------------------ */ +static int16_t *sample_data = NULL; +static int sample_frames = 0; /* stereo frames */ +static int sample_is_external = 0; + +/* playback cursors per drive, -1 = not playing */ +static int play_pos[2] = { -1, -1 }; + +/* previous LED states for rising-edge detection */ +static int prev_led[2] = { 0, 0 }; + +/* settings */ +static int floppy_enabled = 1; +static int floppy_volume = 200; /* 0-256 */ + +/* ------------------------------------------------------------------ */ +/* Public API */ +/* ------------------------------------------------------------------ */ + +void floppy_sound_init(const char *system_dir) +{ + char path[512]; + FILE *f; + long size; + + if (!builtin_generated) + generate_builtin(); + + /* try to load external raw PCM */ + if (system_dir) + { + snprintf(path, sizeof(path), "%s/floppy.raw", system_dir); + f = fopen(path, "rb"); + if (f) + { + fseek(f, 0, SEEK_END); + size = ftell(f); + rewind(f); + if (size > 0) + { + sample_data = (int16_t *)malloc((size_t)size); + if (sample_data) + { + size_t nread = fread(sample_data, 1, (size_t)size, f); + if ((long)nread == size) + { + sample_frames = (int)(size / (2 * sizeof(int16_t))); + sample_is_external = 1; + } + else + { + free(sample_data); + sample_data = NULL; + } + } + } + fclose(f); + } + } + + /* fall back to built-in */ + if (!sample_data) + { + sample_data = builtin_pcm; + sample_frames = BUILTIN_SAMPLES; + sample_is_external = 0; + } + + /* reset state */ + play_pos[0] = play_pos[1] = -1; + prev_led[0] = prev_led[1] = 0; +} + +void floppy_sound_set_enabled(int enabled) +{ + floppy_enabled = enabled; +} + +void floppy_sound_set_volume(int vol) +{ + floppy_volume = vol; /* 0-256 */ +} + +void floppy_sound_update_leds(int leda, int ledb) +{ + /* rising edge: drive A */ + if (leda && !prev_led[0]) + play_pos[0] = 0; + /* rising edge: drive B */ + if (ledb && !prev_led[1]) + play_pos[1] = 0; + + prev_led[0] = leda; + prev_led[1] = ledb; +} + +void floppy_sound_mix(int16_t *buf, int frames) +{ + int d, i; + + if (!floppy_enabled || !sample_data) + return; + + for (d = 0; d < 2; d++) + { + if (play_pos[d] < 0) + continue; + + for (i = 0; i < frames && play_pos[d] < sample_frames; i++, play_pos[d]++) + { + int l = (int)buf[i * 2 ] + ((int)sample_data[play_pos[d] * 2 ] * floppy_volume >> 8); + int r = (int)buf[i * 2 + 1] + ((int)sample_data[play_pos[d] * 2 + 1] * floppy_volume >> 8); + /* clamp to int16 range */ + buf[i * 2 ] = (l > 32767) ? 32767 : (l < -32768) ? (int16_t)-32768 : (int16_t)l; + buf[i * 2 + 1] = (r > 32767) ? 32767 : (r < -32768) ? (int16_t)-32768 : (int16_t)r; + } + + if (play_pos[d] >= sample_frames) + play_pos[d] = -1; + } +} + +void floppy_sound_free(void) +{ + if (sample_is_external && sample_data) + free(sample_data); + sample_data = NULL; + sample_frames = 0; + sample_is_external = 0; +} diff --git a/libretro/floppy_sound.h b/libretro/floppy_sound.h new file mode 100644 index 00000000..41981dbd --- /dev/null +++ b/libretro/floppy_sound.h @@ -0,0 +1,22 @@ +#ifndef FLOPPY_SOUND_H +#define FLOPPY_SOUND_H + +#include + +/* Call once after retro_system_directory is known */ +void floppy_sound_init(const char *system_dir); + +/* Call from update_variables() */ +void floppy_sound_set_enabled(int enabled); +void floppy_sound_set_volume(int vol); /* 0-256, default 200 */ + +/* Call each frame with current LED states, BEFORE audio_cb loop */ +void floppy_sound_update_leds(int leda, int ledb); + +/* Mix floppy click into stereo int16 buffer of `frames` stereo frames */ +void floppy_sound_mix(int16_t *buf, int frames); + +/* Call from retro_deinit() */ +void floppy_sound_free(void); + +#endif /* FLOPPY_SOUND_H */ From 3cdc941f5d0c7a62b352062f5d57b5409e8b7fb5 Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:06:20 +0300 Subject: [PATCH 3/8] Update libretro.c --- libretro/libretro.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libretro/libretro.c b/libretro/libretro.c index 5013e401..2590471b 100644 --- a/libretro/libretro.c +++ b/libretro/libretro.c @@ -15,6 +15,7 @@ #include "retro_strings.h" #include "retro_files.h" #include "retro_disk_control.h" +#include "floppy_sound.h" static dc_storage* dc; @@ -33,6 +34,7 @@ int retroh=248; extern unsigned short int bmp[1024*1024]; extern int STATUTON, SHOWKEY, SHIFTON, MOUSEMODE, PAS, SND; +extern int LEDA, LEDB, LEDC; extern int pauseg, snd_sampler; extern short signed int SNDBUF[1024*2]; extern char RPATH[RETRO_PATH_MAX]; @@ -838,6 +840,17 @@ static void update_variables(void) hatari_led_status_display = true; } + // Floppy Sound + var.key = "hatari_floppy_sound"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) + floppy_sound_set_enabled(strcmp(var.value, "true") == 0); + + var.key = "hatari_floppy_sound_volume"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) + floppy_sound_set_volume(atoi(var.value) * 256 / 100); + // Set frameskip var.key = "hatari_frameskips"; var.value = NULL; @@ -1350,6 +1363,8 @@ void retro_init(void) log_cb(RETRO_LOG_INFO, "Retro SAVE_DIRECTORY %s\n",retro_save_directory); log_cb(RETRO_LOG_INFO, "Retro CONTENT_DIRECTORY %s\n",retro_content_directory); + floppy_sound_init(retro_system_directory); + enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_RGB565; if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { @@ -1383,6 +1398,7 @@ void retro_init(void) void retro_deinit(void) { + floppy_sound_free(); Emu_uninit(); if(emuThread) @@ -1521,6 +1537,9 @@ void retro_run(void) { int16_t *p=(int16_t*)SNDBUF; + floppy_sound_update_leds(LEDA, LEDB); + floppy_sound_mix(p, snd_sampler); + for(x = 0; x < snd_sampler; x++) audio_cb(*p++,*p++); } From a56f3ee3700b287d2830b31d2fbb70922f448ff6 Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:06:48 +0300 Subject: [PATCH 4/8] Update libretro_core_options.h --- libretro/libretro_core_options.h | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/libretro/libretro_core_options.h b/libretro/libretro_core_options.h index c0a1321d..1c3c3c5c 100644 --- a/libretro/libretro_core_options.h +++ b/libretro/libretro_core_options.h @@ -367,6 +367,38 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "true" }, + // Floppy drive sound + { + "hatari_floppy_sound", + "Media -> Floppy Drive Sound", + "Floppy Drive Sound", + "Play mechanical floppy drive sounds on disk access. " + "Place floppy.raw (s16le stereo 44100Hz) in RetroArch system dir for custom sound.", + NULL, + "media", + { + { "true", "Enabled" }, + { "false", "Disabled" }, + { NULL, NULL }, + }, + "true" + }, + { + "hatari_floppy_sound_volume", + "Media -> Floppy Sound Volume", + "Floppy Sound Volume", + "Volume of the floppy drive sound effect.", + NULL, + "media", + { + { "25", "25%" }, + { "50", "50%" }, + { "75", "75%" }, + { "100", "100%" }, + { NULL, NULL }, + }, + "75" + }, // Floppy speed { "hatari_fastfdc", @@ -1514,6 +1546,38 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "true" }, + // Floppy drive sound + { + "hatari_floppy_sound", + "Media -> Floppy Drive Sound", + "Floppy Drive Sound", + "Play mechanical floppy drive sounds on disk access. " + "Place floppy.raw (s16le stereo 44100Hz) in RetroArch system dir for custom sound.", + NULL, + "media", + { + { "true", "Enabled" }, + { "false", "Disabled" }, + { NULL, NULL }, + }, + "true" + }, + { + "hatari_floppy_sound_volume", + "Media -> Floppy Sound Volume", + "Floppy Sound Volume", + "Volume of the floppy drive sound effect.", + NULL, + "media", + { + { "25", "25%" }, + { "50", "50%" }, + { "75", "75%" }, + { "100", "100%" }, + { NULL, NULL }, + }, + "75" + }, // Floppy speed { "hatari_fastfdc", From 745d0e2f2df3574365f0fc3a5e90f9cd19995afe Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:07:26 +0300 Subject: [PATCH 5/8] Update Makefile.common --- Makefile.common | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile.common b/Makefile.common index 1b641d87..35fb0ace 100644 --- a/Makefile.common +++ b/Makefile.common @@ -156,6 +156,7 @@ SOURCES_C += $(CPU_PREGEN)/cpudefs.c \ $(LIBRETRO_DIR)/graph.c \ $(LIBRETRO_DIR)/libretro-graph.c \ $(LIBRETRO_DIR)/bmp.c \ + $(LIBRETRO_DIR)/floppy_sound.c \ $(LIBRETRO_DIR)/retro_strings.c \ $(LIBRETRO_DIR)/retro_utils.c \ $(LIBRETRO_DIR)/retro_disk_control.c \ @@ -328,6 +329,7 @@ SOURCES_C += $(CPU_PREGEN)/cpudefs.c \ $(LIBRETRO_DIR)/graph.c \ $(LIBRETRO_DIR)/libretro-graph.c \ $(LIBRETRO_DIR)/bmp.c \ + $(LIBRETRO_DIR)/floppy_sound.c \ $(LIBRETRO_DIR)/retro_strings.c \ $(LIBRETRO_DIR)/retro_utils.c \ $(LIBRETRO_DIR)/retro_disk_control.c \ From 055eb3b399ba2f261579b46c3d0e165a532ee633 Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:09:15 +0300 Subject: [PATCH 6/8] Update libretro.c --- libretro/libretro.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libretro/libretro.c b/libretro/libretro.c index 2590471b..813aecee 100644 --- a/libretro/libretro.c +++ b/libretro/libretro.c @@ -11,6 +11,7 @@ #include "memorySnapShot.h" #include "main.h" #include "screen.h" +#include "m68000.h" #include "retro_strings.h" #include "retro_files.h" @@ -875,6 +876,19 @@ static void update_variables(void) hatari_fastfdc = new_hatari_fastfdc; ConfigureParams.DiskImage.FastFloppy = hatari_fastfdc; } + + // CPU Speed + var.key = "hatari_cpu_freq"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) + { + int new_freq = atoi(var.value); + if (new_freq != ConfigureParams.System.nCpuFreq) + { + ConfigureParams.System.nCpuFreq = new_freq; + M68000_CheckCpuSettings(); + } + } // Auto Insert Disk B var.key = "hatari_autoloadb"; From a8d2a1d44c9ad9330b7bd00c071dd29261bd39d6 Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:09:50 +0300 Subject: [PATCH 7/8] add 68000 8/16 Mhz switch --- libretro/libretro_core_options.h | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libretro/libretro_core_options.h b/libretro/libretro_core_options.h index 1c3c3c5c..7259aab2 100644 --- a/libretro/libretro_core_options.h +++ b/libretro/libretro_core_options.h @@ -414,6 +414,22 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "true" }, + // CPU speed + { + "hatari_cpu_freq", + "System -> CPU Speed", + "CPU Speed", + "Set 68000 CPU clock speed. 8 MHz is authentic ST hardware speed. " + "16 MHz doubles speed for faster loading and gameplay.", + NULL, + "system", + { + { "8", "8 MHz (Authentic)" }, + { "16", "16 MHz (Fast)" }, + { NULL, NULL }, + }, + "8" + }, // Autoload Drive B { "hatari_autoloadb", @@ -1593,6 +1609,22 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "true" }, + // CPU speed + { + "hatari_cpu_freq", + "System -> CPU Speed", + "CPU Speed", + "Set 68000 CPU clock speed. 8 MHz is authentic ST hardware speed. " + "16 MHz doubles speed for faster loading and gameplay.", + NULL, + "system", + { + { "8", "8 MHz (Authentic)" }, + { "16", "16 MHz (Fast)" }, + { NULL, NULL }, + }, + "8" + }, // Autoload Drive B { "hatari_autoloadb", From 002ff50cd0f155ff906ad3fc0f4d75fbc350635f Mon Sep 17 00:00:00 2001 From: metallic77 <43163462+metallic77@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:46:09 +0300 Subject: [PATCH 8/8] fix vkbd touch bug, it was pressed only one time --- libretro/vkbd.c | 476 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) diff --git a/libretro/vkbd.c b/libretro/vkbd.c index f45de0c7..a21451c9 100644 --- a/libretro/vkbd.c +++ b/libretro/vkbd.c @@ -393,6 +393,482 @@ void input_vkbd(void) if (vkey_sticky1_release) { vkey_sticky1 = -1; vkey_sticky1_release = false; } if (vkey_sticky2_release) { vkey_sticky2 = -1; vkey_sticky2_release = false; } + /* Pointer / touchscreen */ + { + int p_x = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + int p_y = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + int p_p = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED); + + /* Always track pressed state so rising edge is never missed, + even when coordinates return to 0,0 on finger lift */ + if (!p_p) + { + last_pointer_x = 0; + last_pointer_y = 0; + last_pointer_p = 0; + } + + /* Activate pointer on first press, deactivate on joypad use */ + if (!last_pointer_p && p_p) + pointer_active = true; + + if (pointer_active && p_p && + (p_x != last_pointer_x || p_y != last_pointer_y || p_p != last_pointer_p)) + { + /* Convert -0x7fff..0x7fff pointer range to screen coords */ + int px = (int)((p_x + 0x7fff) * retrow / 0xffff); + int py = (int)((p_y + 0x7fff) * retroh / 0xffff); + + if (px >= vkbd_x_min && px <= vkbd_x_max && + py >= vkbd_y_min && py <= vkbd_y_max) + { + float kw = (float)(vkbd_x_max - vkbd_x_min) / VKBDX; + float kh = (float)(vkbd_y_max - vkbd_y_min) / VKBDY; + int nx = (int)((px - vkbd_x_min) / kw); + int ny = (int)((py - vkbd_y_min) / kh); + nx = (nx < 0) ? 0 : (nx > VKBDX-1) ? VKBDX-1 : nx; + ny = (ny < 0) ? 0 : (ny > VKBDY-1) ? VKBDY-1 : ny; + vkey_pos_x = nx; + vkey_pos_y = ny; + + /* Rising edge: finger just touched */ + if (!last_pointer_p && p_p) + { + vkflag[4] = 1; + vkey_pressed_time = frame_count; + } + } + + last_pointer_x = p_x; + last_pointer_y = p_y; + last_pointer_p = p_p; + } + + /* Falling edge: finger lifted — fire the key regardless of coords */ + if (last_pointer_p && !p_p) + { + if (vkflag[4] == 1) + { + vkflag[4] = 0; + int page = vkbd_page ? VKBDX * VKBDY : 0; + int idx = vkey_pos_y * VKBDX + vkey_pos_x + page; + int val = st_vkeys[idx].val; + if (val > 0) + { + st_key_down(val); + last_vkey_pressed = val; + vkey_pressed = val; + vkey_pressed_hold = VKEY_PRESS_FRAMES; /* auto-release after N frames */ + } + else if (val == VKBD_PAGE2) { vkbd_page = !vkbd_page; texture_init(); } + else if (val == VKBD_HIDE) { toggle_vkbd(); } + else if (val == VKBD_STAT) { STATUTON = -STATUTON; } + else if (val == VKBD_COLOR) { opt_vkbd_theme = (opt_vkbd_theme % 3) + 1; } + } + last_pointer_x = 0; + last_pointer_y = 0; + last_pointer_p = 0; + } + } + + /* Draw */ + print_vkbd(); +} +/* + * Hatari libretro - Virtual Keyboard + * Ported and adapted from PUAE libretro (libretro-vkbd.c) + * Original: Copyright 2020-2023 PUAE libretro contributors + * ST layout and adaptation for Hatari + */ + +#include +#include +#include + +#include "libretro.h" +#include "libretro-hatari.h" +#include "libretro-graph.h" +#include "vkbd.h" +#include "vkbd_def.h" + +/* Externals from hatari-mapper / libretro.c */ +extern unsigned short int bmp[]; +extern int retrow, retroh; +extern int CROP_WIDTH; +extern int STATUTON, SHOWKEY, MOUSEMODE; +extern retro_input_state_t input_state_cb; +extern void Screen_SetFullUpdate(void); +extern void texture_init(void); + +/* Public state */ +bool vkbd_active = false; +bool retro_capslock = false; +signed char vkbd_ready = 0; +int vkflag[10] = {0}; + +/* Options */ +unsigned int opt_vkbd_theme = 1; /* 1=classic beige, 2=dark, 3=light */ +unsigned int opt_vkbd_alpha = 255; /* opacity 0-255 */ + +/* Internal state */ +static bool vkbd_page = false; +static int vkey_pos_x = 0; +static int vkey_pos_y = 0; + +static int vkey_pressed = -1; +static int last_vkey_pressed = -1; +static int vkey_pressed_hold = 0; +#define VKEY_PRESS_FRAMES 4 /* hold key for 4 frames ~80ms */ +static bool vkey_sticky = false; +static int vkey_sticky1 = -1; +static int vkey_sticky2 = -1; +static bool vkey_sticky1_release = false; +static bool vkey_sticky2_release = false; + +static long vkey_pressed_time = 0; +static bool vkey_press_hold = false; +#define VKEY_HOLD_DELAY 600 /* ms before sticky activates */ + +/* Pointer/touch state */ +static int last_pointer_x = 0; +static int last_pointer_y = 0; +static int last_pointer_p = 0; +static bool pointer_active = false; +static int vkbd_x_min = 0, vkbd_x_max = 0; +static int vkbd_y_min = 0, vkbd_y_max = 0; + +/* Forward declarations */ +static void st_key_down(int scancode); +static void st_key_up(int scancode); + +/* Drawing via libretro-graph.c */ + +/* ------------------------------------------------------------------ + * Theme colors (RGB565) + * ------------------------------------------------------------------ */ +#define C_BEIGE RGBc(208, 208, 202) +#define C_BEIGE_DRK RGBc(154, 154, 150) +#define C_DARK RGBc( 32, 32, 32) +#define C_DARK_ALT RGBc( 64, 64, 64) +#define C_LIGHT RGBc(220, 220, 220) +#define C_LIGHT_ALT RGBc(160, 160, 160) +#define C_SEL_DARK RGBc(180, 180, 180) +#define C_SEL_LIGHT RGBc( 40, 40, 40) +#define C_EXTRA RGBc(100, 100, 100) +#define C_WHITE RGBc(255, 255, 255) +#define C_BLACK RGBc( 5, 5, 5) +#define C_ACTIVE RGBc(250, 250, 250) + +/* ------------------------------------------------------------------ + * print_vkbd - draws the keyboard onto bmp[] + * ------------------------------------------------------------------ */ +void print_vkbd(void) +{ + int x, y; + int page = vkbd_page ? VKBDX * VKBDY : 0; + + /* Theme */ + unsigned short BKG_NORMAL, BKG_ALT, BKG_SEL, BKG_EXTRA, BKG_ACTIVE; + unsigned short FNT_NORMAL, FNT_SEL; + + switch (opt_vkbd_theme) + { + default: + case 1: /* Classic beige */ + BKG_NORMAL = C_BEIGE; BKG_ALT = C_BEIGE_DRK; + BKG_SEL = C_SEL_LIGHT; BKG_EXTRA = C_EXTRA; + BKG_ACTIVE = C_ACTIVE; + FNT_NORMAL = C_BLACK; FNT_SEL = C_WHITE; + break; + case 2: /* Dark */ + BKG_NORMAL = C_DARK; BKG_ALT = C_DARK_ALT; + BKG_SEL = C_SEL_DARK; BKG_EXTRA = C_EXTRA; + BKG_ACTIVE = RGBc( 10, 10, 10); + FNT_NORMAL = C_WHITE; FNT_SEL = C_BLACK; + break; + case 3: /* Light */ + BKG_NORMAL = C_LIGHT; BKG_ALT = C_LIGHT_ALT; + BKG_SEL = C_SEL_LIGHT; BKG_EXTRA = C_EXTRA; + BKG_ACTIVE = C_ACTIVE; + FNT_NORMAL = C_BLACK; FNT_SEL = C_WHITE; + break; + } + + /* Scale font to resolution */ + int FONT_W = (CROP_WIDTH >= 640) ? 2 : 1; + int FONT_H = FONT_W; + + int VK_XSIDE = (320 * FONT_W) / VKBDX; + int VK_YSIDE = (170 * FONT_H) / VKBDY; + + int XPAD = retrow - (VK_XSIDE * VKBDX); + int YPAD = retroh - (VK_YSIDE * VKBDY); + + int VK_XBASE = (XPAD > 0) ? XPAD / 2 : 0; + int VK_YBASE = (YPAD > 0) ? YPAD / 2 : 0; + + int VK_XTEXT = VK_XBASE + (2 * FONT_W); + int VK_YTEXT = VK_YBASE + (2 * FONT_H); + + int SPACING = 1; + + /* Store bounds for pointer hit testing */ + vkbd_x_min = VK_XBASE; + vkbd_x_max = VK_XBASE + VK_XSIDE * VKBDX; + vkbd_y_min = VK_YBASE; + vkbd_y_max = VK_YBASE + VK_YSIDE * VKBDY; + + /* Draw keys */ + for (x = 0; x < VKBDX; x++) + { + for (y = 0; y < VKBDY; y++) + { + int idx = y * VKBDX + x + page; + int val = st_vkeys[idx].val; + + /* Skip empty keys */ + if (val == -1 && st_vkeys[idx].normal[0] == '\0') + continue; + + /* Pick key color */ + unsigned short key_color; + bool is_selected = (x == vkey_pos_x && y == vkey_pos_y); + + if (is_selected) + key_color = BKG_SEL; + else if (val < 0) + key_color = BKG_EXTRA; + else if (val == SK_LSHIFT || val == SK_RSHIFT || val == SK_CTRL || + val == SK_ALT || val == SK_CAPS) + { + /* Highlight active modifiers */ + bool active = false; + if (val == SK_LSHIFT && vkey_sticky1 == SK_LSHIFT) active = true; + if (val == SK_RSHIFT && vkey_sticky1 == SK_RSHIFT) active = true; + if (val == SK_CTRL && vkey_sticky1 == SK_CTRL) active = true; + if (val == SK_ALT && vkey_sticky1 == SK_ALT) active = true; + if (val == SK_LSHIFT && vkey_sticky2 == SK_LSHIFT) active = true; + if (val == SK_CAPS && retro_capslock) active = true; + key_color = active ? BKG_ACTIVE : BKG_ALT; + } + else + key_color = (y % 2) ? BKG_NORMAL : BKG_ALT; + + int kx = VK_XBASE + x * VK_XSIDE; + int ky = VK_YBASE + y * VK_YSIDE; + + /* Fill + border using libretro-graph */ + draw_fbox(kx + SPACING, ky + SPACING, + VK_XSIDE - SPACING*2, VK_YSIDE - SPACING*2, + key_color, GRAPH_ALPHA_75); + draw_box(kx + SPACING, ky + SPACING, + VK_XSIDE - SPACING*2, VK_YSIDE - SPACING*2, + retrow, retroh, + is_selected ? C_WHITE : C_BLACK, GRAPH_ALPHA_100); + + /* Label */ + bool shifted = (vkey_sticky1 == SK_LSHIFT || vkey_sticky1 == SK_RSHIFT || + vkey_sticky2 == SK_LSHIFT || vkey_sticky2 == SK_RSHIFT); + const char *label = shifted ? st_vkeys[idx].shift : st_vkeys[idx].normal; + + draw_text(VK_XTEXT + x * VK_XSIDE, VK_YTEXT + y * VK_YSIDE, + is_selected ? FNT_SEL : FNT_NORMAL, + key_color, + GRAPH_ALPHA_100, GRAPH_BG_NONE, + FONT_W, FONT_H, 4, + (const unsigned char *)label); + } + } +} + +/* ------------------------------------------------------------------ + * Key send helpers + * ------------------------------------------------------------------ */ +static void st_key_down(int scancode) +{ + if (scancode <= 0) return; + IKBD_PressSTKey((unsigned char)scancode, true); +} + +static void st_key_up(int scancode) +{ + if (scancode <= 0) return; + IKBD_PressSTKey((unsigned char)scancode, false); +} + +/* ------------------------------------------------------------------ + * toggle_vkbd + * ------------------------------------------------------------------ */ +void toggle_vkbd(void) +{ + vkbd_active = !vkbd_active; + SHOWKEY = vkbd_active ? 1 : -1; + texture_init(); + Screen_SetFullUpdate(); +} + +/* ------------------------------------------------------------------ + * input_vkbd - called every frame when vkbd is visible + * ------------------------------------------------------------------ */ +void input_vkbd(void) +{ + static long frame_count = 0; + frame_count++; + + /* --- Navigation --- */ + if (input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP) && vkflag[0] == 0) + vkflag[0] = 1; + else if (!input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP) && vkflag[0] == 1) + { + vkflag[0] = 0; + vkey_pos_y = (vkey_pos_y - 1 + VKBDY) % VKBDY; + } + + if (input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN) && vkflag[1] == 0) + vkflag[1] = 1; + else if (!input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN) && vkflag[1] == 1) + { + vkflag[1] = 0; + vkey_pos_y = (vkey_pos_y + 1) % VKBDY; + } + + if (input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT) && vkflag[2] == 0) + vkflag[2] = 1; + else if (!input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT) && vkflag[2] == 1) + { + vkflag[2] = 0; + vkey_pos_x = (vkey_pos_x - 1 + VKBDX) % VKBDX; + } + + if (input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT) && vkflag[3] == 0) + vkflag[3] = 1; + else if (!input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT) && vkflag[3] == 1) + { + vkflag[3] = 0; + vkey_pos_x = (vkey_pos_x + 1) % VKBDX; + } + + /* Analog stick navigation */ + int ax = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); + int ay = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); + const int THRESH = 20000; + + if (ay < -THRESH && vkflag[5] == 0) { vkflag[5] = 1; } + else if (ay >= -THRESH && vkflag[5] == 1) { vkflag[5] = 0; vkey_pos_y = (vkey_pos_y - 1 + VKBDY) % VKBDY; } + + if (ay > THRESH && vkflag[6] == 0) { vkflag[6] = 1; } + else if (ay <= THRESH && vkflag[6] == 1) { vkflag[6] = 0; vkey_pos_y = (vkey_pos_y + 1) % VKBDY; } + + if (ax < -THRESH && vkflag[7] == 0) { vkflag[7] = 1; } + else if (ax >= -THRESH && vkflag[7] == 1) { vkflag[7] = 0; vkey_pos_x = (vkey_pos_x - 1 + VKBDX) % VKBDX; } + + if (ax > THRESH && vkflag[8] == 0) { vkflag[8] = 1; } + else if (ax <= THRESH && vkflag[8] == 1) { vkflag[8] = 0; vkey_pos_x = (vkey_pos_x + 1) % VKBDX; } + + /* --- Key press (B button) --- */ + bool b_pressed = input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B); + + if (b_pressed && vkflag[4] == 0) + { + vkflag[4] = 1; + vkey_pressed_time = frame_count; + vkey_press_hold = false; + } + else if (b_pressed && vkflag[4] == 1) + { + /* Check for long press -> sticky */ + if (!vkey_press_hold && (frame_count - vkey_pressed_time) > 30) + { + vkey_press_hold = true; + vkey_sticky = true; + } + } + else if (!b_pressed && vkflag[4] == 1) + { + vkflag[4] = 0; + + int page = vkbd_page ? VKBDX * VKBDY : 0; + int idx = vkey_pos_y * VKBDX + vkey_pos_x + page; + int val = st_vkeys[idx].val; + + if (val == VKBD_PAGE2) + { + vkbd_page = !vkbd_page; + texture_init(); + } + else if (val == VKBD_HIDE) + { + toggle_vkbd(); + } + else if (val == VKBD_STAT) + { + STATUTON = -STATUTON; + } + else if (val == VKBD_COLOR) + { + /* cycle theme */ + opt_vkbd_theme = (opt_vkbd_theme % 3) + 1; + } + else if (val > 0) + { + /* Modifier keys are always sticky (toggle on/off) */ + if (val == SK_LSHIFT || val == SK_RSHIFT || + val == SK_CTRL || val == SK_ALT || val == SK_CAPS) + { + if (vkey_sticky1 == val) + { + st_key_up(val); + vkey_sticky1 = -1; + } + else if (vkey_sticky2 == val) + { + st_key_up(val); + vkey_sticky2 = -1; + } + else + { + if (vkey_sticky1 == -1) + vkey_sticky1 = val; + else + vkey_sticky2 = val; + st_key_down(val); + } + } + else + { + /* Normal key: press down now, release after VKEY_PRESS_FRAMES */ + st_key_down(val); + last_vkey_pressed = val; + vkey_pressed = val; + vkey_pressed_hold = VKEY_PRESS_FRAMES; + } + } + + vkey_sticky = false; + } + + /* Release normal key after hold frames */ + if (vkey_pressed != -1 && !vkflag[4]) + { + if (vkey_pressed_hold > 0) + { + vkey_pressed_hold--; + } + else + { + st_key_up(vkey_pressed); + + /* Release sticky modifiers */ + if (vkey_sticky1 > 0) { st_key_up(vkey_sticky1); vkey_sticky1_release = true; } + if (vkey_sticky2 > 0) { st_key_up(vkey_sticky2); vkey_sticky2_release = true; } + + vkey_pressed = -1; + } + } + + if (vkey_sticky1_release) { vkey_sticky1 = -1; vkey_sticky1_release = false; } + if (vkey_sticky2_release) { vkey_sticky2 = -1; vkey_sticky2_release = false; } + /* Pointer / touchscreen */ { int p_x = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X);