From bf56a071d28b6af21e07bc360c8c5d00ca1c5d07 Mon Sep 17 00:00:00 2001 From: Garretonzo Date: Wed, 9 Sep 2026 21:39:53 -0700 Subject: [PATCH] store_or_get_action - cache keycode on key press, return cached keycode on key release (#25349) Co-authored-by: Nick Brassel --- builddefs/build_test.mk | 1 + builddefs/testlist.mk | 1 + docs/config_options.md | 4 + quantum/action_layer.c | 95 +++++++- quantum/action_layer.h | 3 + quantum/keycode_cache/keycode_cache_mock.c | 85 +++++++ quantum/keycode_cache/keycode_cache_mock.h | 36 +++ quantum/keycode_cache/keycode_cache_tests.cpp | 220 ++++++++++++++++++ quantum/keycode_cache/tests/rules.mk | 8 + quantum/keycode_cache/tests/testlist.mk | 2 + 10 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 quantum/keycode_cache/keycode_cache_mock.c create mode 100644 quantum/keycode_cache/keycode_cache_mock.h create mode 100644 quantum/keycode_cache/keycode_cache_tests.cpp create mode 100644 quantum/keycode_cache/tests/rules.mk create mode 100644 quantum/keycode_cache/tests/testlist.mk diff --git a/builddefs/build_test.mk b/builddefs/build_test.mk index 1f1a9fbea83..a9c9e5be2ee 100644 --- a/builddefs/build_test.mk +++ b/builddefs/build_test.mk @@ -67,6 +67,7 @@ include $(QUANTUM_PATH)/debounce/tests/rules.mk include $(QUANTUM_PATH)/encoder/tests/rules.mk include $(QUANTUM_PATH)/os_detection/tests/rules.mk include $(QUANTUM_PATH)/sequencer/tests/rules.mk +include $(QUANTUM_PATH)/keycode_cache/tests/rules.mk include $(QUANTUM_PATH)/wear_leveling/tests/rules.mk include $(QUANTUM_PATH)/logging/print.mk include $(PLATFORM_PATH)/test/rules.mk diff --git a/builddefs/testlist.mk b/builddefs/testlist.mk index 2e81fe576bf..347b979ba9c 100644 --- a/builddefs/testlist.mk +++ b/builddefs/testlist.mk @@ -6,6 +6,7 @@ include $(QUANTUM_PATH)/debounce/tests/testlist.mk include $(QUANTUM_PATH)/encoder/tests/testlist.mk include $(QUANTUM_PATH)/os_detection/tests/testlist.mk include $(QUANTUM_PATH)/sequencer/tests/testlist.mk +include $(QUANTUM_PATH)/keycode_cache/tests/testlist.mk include $(QUANTUM_PATH)/wear_leveling/tests/testlist.mk include $(PLATFORM_PATH)/test/testlist.mk diff --git a/docs/config_options.md b/docs/config_options.md index fa18b4c7d16..23210447b9d 100644 --- a/docs/config_options.md +++ b/docs/config_options.md @@ -124,6 +124,10 @@ If you define these options you will enable the associated feature, which may in * Enables the `QK_MAKE` keycode * `#define STRICT_LAYER_RELEASE` * force a key release to be evaluated using the current layer stack instead of remembering which layer it came from (used for advanced cases) +* `#define KEYCODE_CACHE_ENABLE` + * Cache keycode for pressed keys, to be used on key release, across entire physical keyboard layout. +* `#define KEYCODE_CACHE_LIMIT 10` + * Allows for changing the limit of the number of keycodes able to be cached. Keys pressed beyond the limit will behave as if the keycode cache were disabled. Default when unspecified is 10. ## Behaviors That Can Be Configured diff --git a/quantum/action_layer.c b/quantum/action_layer.c index 0a088ab5f0c..a0a2ed9bdad 100644 --- a/quantum/action_layer.c +++ b/quantum/action_layer.c @@ -306,6 +306,76 @@ uint8_t read_source_layers_cache(keypos_t key) { # endif // ENCODER_MAP_ENABLE return 0; } + +# ifdef KEYCODE_CACHE_ENABLE +# ifndef KEYCODE_CACHE_LIMIT +# define KEYCODE_CACHE_LIMIT 10 +# endif + +typedef struct historical_keycode_t { + uint8_t row; + uint8_t col; + uint16_t keycode; +} historical_keycode_t; +static historical_keycode_t keycode_cache[KEYCODE_CACHE_LIMIT]; +static uint16_t keycode_cache_count = 0; + +/** \brief find keycode cache index + * + * returns index of keycode_cache for given key + */ +static int16_t find_keycode_cache_index(keypos_t key) { + for (uint16_t keycode_cache_idx = 0; keycode_cache_idx < keycode_cache_count; ++keycode_cache_idx) { + if (keycode_cache[keycode_cache_idx].row == key.row && keycode_cache[keycode_cache_idx].col == key.col) { + return keycode_cache_idx; + } + } + return -1; +} + +/** \brief add keycode cache + * + * add to cache of keycodes after a key is pressed down + */ +static void add_keycode_cache(keypos_t key, uint16_t keycode) { + int16_t keycode_cache_idx = find_keycode_cache_index(key); + if (keycode_cache_idx >= 0) { + keycode_cache[keycode_cache_idx].keycode = keycode; + return; + } + if (keycode_cache_count < KEYCODE_CACHE_LIMIT) { + keycode_cache[keycode_cache_count].row = key.row; + keycode_cache[keycode_cache_count].col = key.col; + keycode_cache[keycode_cache_count].keycode = keycode; + keycode_cache_count++; + } +} + +/** \brief remove keycode cache + * + * remove from cache of keycodes after a key is released + */ +static void remove_keycode_cache(keypos_t key) { + int16_t keycode_cache_idx = find_keycode_cache_index(key); + if (keycode_cache_idx >= 0) { + keycode_cache[keycode_cache_idx] = keycode_cache[keycode_cache_count - 1]; + keycode_cache_count--; + } +} + +/** \brief read keycode cache + * + * reads from cache of keycodes for when a key is releasing + */ +static uint16_t read_keycode_cache(keypos_t key) { + int16_t keycode_cache_idx = find_keycode_cache_index(key); + if (keycode_cache_idx >= 0) { + return keycode_cache[keycode_cache_idx].keycode; + } + return KC_NO; +} +# endif + #endif /** \brief Store or get action (FIXME: Needs better summary) @@ -322,14 +392,37 @@ action_t store_or_get_action(bool pressed, keypos_t key) { } uint8_t layer; - +# ifdef KEYCODE_CACHE_ENABLE + uint16_t keycode; + bool cache_used = false; +# endif if (pressed) { layer = layer_switch_get_layer(key); update_source_layers_cache(key, layer); +# ifdef KEYCODE_CACHE_ENABLE + keycode = keymap_key_to_keycode(layer, key); + if (keycode_cache_count < KEYCODE_CACHE_LIMIT) { + add_keycode_cache(key, keycode); + cache_used = true; + } +# endif } else { layer = read_source_layers_cache(key); +# ifdef KEYCODE_CACHE_ENABLE + keycode = read_keycode_cache(key); + remove_keycode_cache(key); + cache_used = (keycode != KC_NO); +# endif } +# ifndef KEYCODE_CACHE_ENABLE return action_for_key(layer, key); +# else + if (cache_used) { + return action_for_keycode(keycode); + } else { + return action_for_key(layer, key); + } +# endif #else return layer_switch_get_action(key); #endif diff --git a/quantum/action_layer.h b/quantum/action_layer.h index 067e33cdb5c..388f96e9c81 100644 --- a/quantum/action_layer.h +++ b/quantum/action_layer.h @@ -163,6 +163,9 @@ layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_ void update_source_layers_cache(keypos_t key, uint8_t layer); uint8_t read_source_layers_cache(keypos_t key); +# ifdef KEYCODE_CACHE_ENABLE +uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key); +# endif #endif action_t store_or_get_action(bool pressed, keypos_t key); diff --git a/quantum/keycode_cache/keycode_cache_mock.c b/quantum/keycode_cache/keycode_cache_mock.c new file mode 100644 index 00000000000..7351e1106b8 --- /dev/null +++ b/quantum/keycode_cache/keycode_cache_mock.c @@ -0,0 +1,85 @@ +/* Copyright 2025 Garretonzo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "keycode_cache_mock.h" +#include "quantum/action.h" +#include "quantum/quantum_keycodes.h" + +// Mock keymap storage +static uint16_t mock_keymap[4][4] = {{KC_NO}}; + +// Mock functions needed for testing +uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key) { + if (layer < 4 && key.row < 4 && key.col < 4) { + return mock_keymap[key.row][key.col]; + } + return KC_NO; +} + +void dynamic_keymap_set_keycode(uint8_t layer, uint8_t row, uint8_t column, uint16_t keycode) { + if (layer < 4 && row < 4 && column < 4) { + mock_keymap[row][column] = keycode; + } +} + +uint16_t dynamic_keymap_get_keycode(uint8_t layer, uint8_t row, uint8_t column) { + if (layer < 4 && row < 4 && column < 4) { + return mock_keymap[row][column]; + } + return KC_NO; +} + +action_t action_for_keycode(uint16_t keycode) { + action_t action; + action.code = keycode; + return action; +} + +action_t action_for_key(uint8_t layer, keypos_t key) { + uint16_t keycode = keymap_key_to_keycode(layer, key); + return action_for_keycode(keycode); +} + +// Utilities for debug +uint8_t biton(uint8_t bits) { + for (uint8_t i = 0; i < 8; i++) { + if (bits & (1 << i)) return i; + } + return 0; +} + +uint8_t biton16(uint16_t bits) { + for (uint8_t i = 0; i < 16; i++) { + if (bits & (1 << i)) return i; + } + return 0; +} + +uint8_t biton32(uint32_t bits) { + for (uint8_t i = 0; i < 32; i++) { + if (bits & (1UL << i)) return i; + } + return 0; +} + +// Keyboard functions +void clear_keyboard_but_mods(void) { + // No-op for tests +} + +void clear_keyboard_but_mods_and_keys(void) { + // No-op for tests +} diff --git a/quantum/keycode_cache/keycode_cache_mock.h b/quantum/keycode_cache/keycode_cache_mock.h new file mode 100644 index 00000000000..48cc65fd950 --- /dev/null +++ b/quantum/keycode_cache/keycode_cache_mock.h @@ -0,0 +1,36 @@ +/* Copyright 2025 Garretonzo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "quantum/action.h" + +// Mock function declarations +uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key); +void dynamic_keymap_set_keycode(uint8_t layer, uint8_t row, uint8_t column, uint16_t keycode); +uint16_t dynamic_keymap_get_keycode(uint8_t layer, uint8_t row, uint8_t column); +action_t action_for_keycode(uint16_t keycode); +action_t action_for_key(uint8_t layer, keypos_t key); + +// Utility functions +uint8_t biton(uint8_t bits); +uint8_t biton16(uint16_t bits); +uint8_t biton32(uint32_t bits); + +// Keyboard functions +void clear_keyboard_but_mods(void); +void clear_keyboard_but_mods_and_keys(void); diff --git a/quantum/keycode_cache/keycode_cache_tests.cpp b/quantum/keycode_cache/keycode_cache_tests.cpp new file mode 100644 index 00000000000..b9748de72b4 --- /dev/null +++ b/quantum/keycode_cache/keycode_cache_tests.cpp @@ -0,0 +1,220 @@ +/* Copyright 2025 Garretonzo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gtest/gtest.h" + +extern "C" { +#include "quantum/action_layer.h" +#include "quantum/action.h" +#include "quantum/dynamic_keymap.h" +#include "quantum/quantum_keycodes.h" +#include "quantum/keymap_common.h" +#include "keycode_cache_mock.h" +} + +// Mock variables for testing +extern "C" { +// These need to be defined for the tests to compile +bool disable_action_cache = false; +} + +class KeycodeCacheTest : public ::testing::Test { + protected: + void SetUp() override { + // Reset any global state before each test + disable_action_cache = false; + + // Initialize test key position + test_key.row = 0; + test_key.col = 0; + + // Initialize test keycodes + initial_keycode = KC_A; + changed_keycode = KC_B; + + // Set initial keymap + dynamic_keymap_set_keycode(0, test_key.row, test_key.col, initial_keycode); + } + + void TearDown() override { + // Clean up after each test + disable_action_cache = false; + } + + keypos_t test_key; + uint16_t initial_keycode; + uint16_t changed_keycode; +}; + +#ifdef KEYCODE_CACHE_ENABLE + +TEST_F(KeycodeCacheTest, TestKeycodeMatchOnPressAndRelease) { + // Test that store_or_get_action works with keycode cache enabled + // The keycode when pressed should match the keycode when released + + // Press the key + action_t press_action = store_or_get_action(true, test_key); + + // Release the key + action_t release_action = store_or_get_action(false, test_key); + + // Both actions should be for the same keycode + EXPECT_EQ(press_action.code, release_action.code); + EXPECT_EQ(press_action.code, action_for_keycode(initial_keycode).code); +} + +TEST_F(KeycodeCacheTest, TestCacheLimitExceeded) { + // Test that when keycode cache limit is exceeded, + // it falls back to default functionality + + // Fill the cache to the limit + keypos_t keys[KEYCODE_CACHE_LIMIT + 1]; + for (int i = 0; i <= KEYCODE_CACHE_LIMIT; i++) { + keys[i].row = i / MATRIX_COLS; + keys[i].col = i % MATRIX_COLS; + dynamic_keymap_set_keycode(0, keys[i].row, keys[i].col, KC_A + i); + + // Press each key + store_or_get_action(true, keys[i]); + } + + // The last key should not be cached (exceeds limit) + // Change its keycode + uint16_t new_keycode = KC_Z; + dynamic_keymap_set_keycode(0, keys[KEYCODE_CACHE_LIMIT].row, keys[KEYCODE_CACHE_LIMIT].col, new_keycode); + + // Release the last key - should get the NEW keycode (not cached) + action_t release_action = store_or_get_action(false, keys[KEYCODE_CACHE_LIMIT]); + EXPECT_EQ(release_action.code, action_for_keycode(new_keycode).code); + + // Clean up - release all other keys + for (int i = 0; i < KEYCODE_CACHE_LIMIT; i++) { + store_or_get_action(false, keys[i]); + } +} + +TEST_F(KeycodeCacheTest, TestDynamicKeymapChangeWithCache) { + // Test with keycode cache: a key is pressed, during the press + // dynamic_keymap_set_keycode remaps the key to a different key, + // then when released, the released key should match the initial key + + // Press the key (should cache initial_keycode) + action_t press_action = store_or_get_action(true, test_key); + EXPECT_EQ(press_action.code, action_for_keycode(initial_keycode).code); + + // While key is pressed, change the keymap + dynamic_keymap_set_keycode(0, test_key.row, test_key.col, changed_keycode); + + // Release the key - should get the ORIGINAL cached keycode + action_t release_action = store_or_get_action(false, test_key); + EXPECT_EQ(release_action.code, action_for_keycode(initial_keycode).code); + EXPECT_NE(release_action.code, action_for_keycode(changed_keycode).code); +} + +#endif // KEYCODE_CACHE_ENABLE + +TEST_F(KeycodeCacheTest, TestDynamicKeymapChangeWithoutCache) { + // Test without keycode cache: a key is pressed, during the press + // dynamic_keymap_set_keycode remaps the key to a different key, + // then when released, the released key should match the NEW key + + // Disable cache for this test + disable_action_cache = true; + + // Press the key + action_t press_action = store_or_get_action(true, test_key); + EXPECT_EQ(press_action.code, action_for_keycode(initial_keycode).code); + + // While key is pressed, change the keymap + dynamic_keymap_set_keycode(0, test_key.row, test_key.col, changed_keycode); + + // Release the key - should get the NEW keycode (no cache) + action_t release_action = store_or_get_action(false, test_key); + EXPECT_EQ(release_action.code, action_for_keycode(changed_keycode).code); + EXPECT_NE(release_action.code, action_for_keycode(initial_keycode).code); +} + +#ifdef KEYCODE_CACHE_ENABLE + +TEST_F(KeycodeCacheTest, TestCacheUpdateExistingKey) { + // Test that pressing the same key twice updates the cache entry + // rather than creating a duplicate + + // Press the key + store_or_get_action(true, test_key); + + // Change the keymap + dynamic_keymap_set_keycode(0, test_key.row, test_key.col, changed_keycode); + + // Press the same key again (should update existing cache entry) + action_t second_press_action = store_or_get_action(true, test_key); + EXPECT_EQ(second_press_action.code, action_for_keycode(changed_keycode).code); + + // Release should get the updated keycode + action_t release_action = store_or_get_action(false, test_key); + EXPECT_EQ(release_action.code, action_for_keycode(changed_keycode).code); +} + +TEST_F(KeycodeCacheTest, TestCacheRemovalOnRelease) { + // Test that releasing a key removes it from the cache + + // Press and release a key + store_or_get_action(true, test_key); + store_or_get_action(false, test_key); + + // Change the keymap + dynamic_keymap_set_keycode(0, test_key.row, test_key.col, changed_keycode); + + // Press the key again - should use new keycode (not cached) + action_t press_action = store_or_get_action(true, test_key); + EXPECT_EQ(press_action.code, action_for_keycode(changed_keycode).code); + + // Clean up + store_or_get_action(false, test_key); +} + +TEST_F(KeycodeCacheTest, TestMultipleKeysInCache) { + // Test that multiple keys can be cached simultaneously + + keypos_t key1 = {0, 0}; + keypos_t key2 = {0, 1}; + uint16_t keycode1 = KC_A; + uint16_t keycode2 = KC_B; + + // Set up keymaps + dynamic_keymap_set_keycode(0, key1.row, key1.col, keycode1); + dynamic_keymap_set_keycode(0, key2.row, key2.col, keycode2); + + // Press both keys + action_t press1 = store_or_get_action(true, key1); + action_t press2 = store_or_get_action(true, key2); + + EXPECT_EQ(press1.code, action_for_keycode(keycode1).code); + EXPECT_EQ(press2.code, action_for_keycode(keycode2).code); + + // Change both keymaps + dynamic_keymap_set_keycode(0, key1.row, key1.col, KC_X); + dynamic_keymap_set_keycode(0, key2.row, key2.col, KC_Y); + + // Release both keys - should get original keycodes + action_t release1 = store_or_get_action(false, key1); + action_t release2 = store_or_get_action(false, key2); + + EXPECT_EQ(release1.code, action_for_keycode(keycode1).code); + EXPECT_EQ(release2.code, action_for_keycode(keycode2).code); +} + +#endif // KEYCODE_CACHE_ENABLE diff --git a/quantum/keycode_cache/tests/rules.mk b/quantum/keycode_cache/tests/rules.mk new file mode 100644 index 00000000000..534610c71fa --- /dev/null +++ b/quantum/keycode_cache/tests/rules.mk @@ -0,0 +1,8 @@ +# Keycode cache unit tests configuration + +keycode_cache_DEFS := -DMATRIX_ROWS=4 -DMATRIX_COLS=4 -DNO_DEBUG -DKEYCODE_CACHE_ENABLE -DKEYCODE_CACHE_LIMIT=8 + +keycode_cache_SRC := \ + $(QUANTUM_PATH)/keycode_cache/keycode_cache_tests.cpp \ + $(QUANTUM_PATH)/keycode_cache/keycode_cache_mock.c \ + $(QUANTUM_PATH)/action_layer.c \ diff --git a/quantum/keycode_cache/tests/testlist.mk b/quantum/keycode_cache/tests/testlist.mk new file mode 100644 index 00000000000..ff59a0944d3 --- /dev/null +++ b/quantum/keycode_cache/tests/testlist.mk @@ -0,0 +1,2 @@ +TEST_LIST += \ + keycode_cache \