← More about keyboards

⚠  Caution

Check game rules before using. Notably, Counter-Strike does not allow SOCD filtering. It is your responsibility to disable SOCD Cleaner where it is prohibited.

“Remember, a kite rises against, not with, the wind.” — Hamilton Wright Mabie

Overview

SOCD Cleaner is a QMK userspace library for Simultaneous Opposing Cardinal Directions (SOCD) filtering. What this mouthful of a name means is that when two keys of opposing direction are held at the same time, a rule is applied to decide which key is sent to the computer. Such filtering is popular for fast inputs on the WASD keys in gaming. See HitBox’s Introduction to SOCD and Resolutions for a detailed description.

SOCD Cleaner with SOCD_CLEANER_LAST resolution.

Capabilities of SOCD Cleaner:

Motivation and status in QMK core

As of August 2024, SOCD filtering is expected to come soon as a core QMK feature as the Key Cancellation feature in PR 24000 together with a following PR. However, as tzarc’s RFC on Key Cancellation explains, there is both significant interest in this feature and general questions about what’s necessary for real usage patterns with SOCD.

I suggest what’s needed in the interim is a userspace library. SOCD Cleaner provides a solution for users interested in getting SOCD filtering now. It also serves as a test bed for exploring what range of SOCD options fit best for in real use. I’ve tried to keep the code short and simple to make it hopefully approachable for others to hack on.

Add it to your keymap

Prerequisites

I’m also supposing that you have created a keymap under directory location

qmk_firmware/keyboards/keyboard-name/keymaps/keymap-name

I’ll refer to this location as the “keymap folder.”

Step 1: rules.mk

In your keymap folder, create a plain text file called rules.mk containing the following line. If such a file already exists, add this as a new line at the bottom of the file.

SRC += features/socd_cleaner.c

This line tells the build system that it must also compile the SOCD Cleaner library when compiling your keymap.

Step 2: library source files

In your keymap folder, create a features subdirectory and copy socd_cleaner.h and socd_cleaner.c there.

The directory structure should be like

Step 3: keymap.c changes

Your keymap folder should contain a file called keymap.c. Edit this file to make the following changes.

At the top of keymap.c, add:

#include "features/socd_cleaner.h"

socd_cleaner_t socd_v = {{KC_W, KC_S}, SOCD_CLEANER_LAST};
socd_cleaner_t socd_h = {{KC_A, KC_D}, SOCD_CLEANER_LAST};

The #include line pulls in the definitions for the SOCD Cleaner for use in your keymap. The following socd_cleaner_t lines prepare to perform SOCD filtering on the WASD keys (referred to by keycodes KC_W, KC_A, KC_S, KC_D) with last input priority resolution (SOCD_CLEANER_LAST). (If you want to do something else, this is where to change that.)

Look through the file to see whether it contains a “process_record_user” function definition. If not, add the following to the bottom of the file:

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (!process_socd_cleaner(keycode, record, &socd_v)) { return false; }
  if (!process_socd_cleaner(keycode, record, &socd_h)) { return false; }

  return true;
}

On the other hand, if the file does already have a “process_record_user” function defined, then add to that existing definition. Insert these two process_socd_cleaner lines at the top of the function, just after the opening { brace, and keep any existing code in the function after that point:

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (!process_socd_cleaner(keycode, record, &socd_v)) { return false; }
  if (!process_socd_cleaner(keycode, record, &socd_h)) { return false; }
  // Existing code after this line...

  return true;
}

Step 4: toggling SOCD Cleaner

By default, SOCD Cleaner is enabled globally. Here are two suggestions to control when SOCD Cleaner is enabled. These are mutually exclusive, choose one or the other, not both:

Beware that some games do not allow SOCD filtering, including Counter-Strike. Check game rules before using. Implement one of the suggestions above or a similar means so that you can disable SOCD Cleaner where it is prohibited.

Testing

Flash the firmware to the keyboard. To test the effect, use the QMK Configurator key tester to see what key events your keyboard is sending. Repeatedly tapping the D key while A is held should send ADADADAD.

SOCD key pairs

Each socd_cleaner_t instance is defined by a pair of basic keycodes and a SOCD resolution strategy (more explanation below). For example, the following line defines W and S as opposing keys with SOCD_CLEANER_LAST resolution:

socd_cleaner_t socd_v = {{KC_W, KC_S}, SOCD_CLEANER_LAST};

📝  Note

Define socd_cleaner_t instances as global variables, outside of process_record_user().

For SOCD filtering of A and D (or any other pair of basic keys), define additional socd_cleaner_t instances for each key pair.

In process_record_user(), call the handler process_socd_cleaner() and pass a pointer to the socd_cleaner_t as the third argument:

if (!process_socd_cleaner(keycode, record, &socd_v)) { return false; }

For multiple key pairs, call process_socd_cleaner() with each instance:

if (!process_socd_cleaner(keycode, record, &socd_v)) { return false; }
if (!process_socd_cleaner(keycode, record, &socd_h)) { return false; }

Resolution strategies

Controls vary across games, and there are different SOCD resolution strategies that may be preferred depending on circumstances. The following resolutions are implemented:

If you don’t know what to pick, SOCD_CLEANER_LAST is recommended.

Dynamic strategy

The resolution strategy on a socd_cleaner_t may be changed at run time by assigning to its .resolution field. Supposing GAME1 and GAME2 are custom keycodes, here is how to switch to a different strategy between games:

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (!process_socd_cleaner(keycode, record, &socd_v)) { return false; }
  if (!process_socd_cleaner(keycode, record, &socd_h)) { return false; }

  switch (keycode) {
    case GAME1:
      if (record->event.pressed) {
        socd_v.resolution = SOCD_CLEANER_LAST;
        socd_h.resolution = SOCD_CLEANER_LAST;
      }
      break;

    case GAME2:
      if (record->event.pressed) {
        socd_v.resolution = SOCD_CLEANER_0_WINS;
        socd_h.resolution = SOCD_CLEANER_NEUTRAL;
      }
      break;

    // Other macros...
  }
  return true;
}

Explanation

If you are interested in the technical details, here is an outline of how SOCD Cleaner processes a key event.

  1. For every key press or release, process_record_user() is called. In turn, process_socd_cleaner() is called for each SOCD key pair.

  2. In each call of process_socd_cleaner(), the function checks whether the event’s keycode matches a keycode in the socd_cleaner_t. If not, the function returns early.

  3. Otherwise, socd_cleaner_t.held is updated to track which keys are physically held. This may generally differ from what keys are being reported to the host computer.

  4. If the key opposing the current event is held, SOCD resolution is needed. Following the logic outlined below, we add or remove keys from the current keyboard report.

Suppose that the current event is on key 0 and the opposing key 1 is held (the reverse roles are similar):

Acknowledgements

Thank you @Xelus22 for spearheading Key Cancellation in QMK core, and thank you u/Memey-al-la-Creamy for valuable feedback. A thank you to @tzarc, @drashna, @ChristopheL92, @henrebotha, @aldehir, @kqxu1017, and others who contributed to discussion of this topic.

← More about keyboards