qmk / qmk_firmware

Open-source keyboard firmware for Atmel AVR and Arm USB families
https://qmk.fm
GNU General Public License v2.0
17.97k stars 38.63k forks source link

[RFC] Key cancellation -- Snap Tap / SOCD / Rappy Snappy et.al. #24216

Open tzarc opened 1 month ago

tzarc commented 1 month ago

In case it's not obvious, this issue is not to be used for support on how to integrate #24000 into your codebase.


We understand there are a significant number of people extremely keen on finding that competitive edge in gaming. Whilst the addition of key cancellation or "special tap modes" may not be as immediate as some of you desire, what we want to do is ensure what's actually implemented is what is actually necessary.

There's an initial PR #24000 already in play, but we've held off on merge because of a few general behaviour questions.

As of today, we have normal keyboard functionality:

Keys   | .. | A. | AD | A.
Report | .. | A. | AD | A.

As far as we can tell, there are three variations on the specialised tapping concept:

Variant 1: cancellation -- currently implemented in #24000:

Keys   | .. | A. | AD | A.
Report | .. | A. | .D | .. ----- (D cancels A, no restore on D keyup)

Variant 2: exclusion:

Keys   | .. | A. | AD | A.
Report | .. | A. | .D | A. ----- (D excludes A, restores A on D keyup)

Variant 3: nullification:

Keys   | .. | A. | AD | A.
Report | .. | A. | .. | A. ----- (D nullifies A, neither registered, A restored on D keyup)

What we don't know is:

Obviously the more complex the configurability of this functionality, the longer it takes to prepare and implement for PR purposes.

Realistic usage patterns, please.

ChristopheL92 commented 1 month ago

Counter Strike player there : Variant 2, exclusion, offers the greatest advantage. Currently, with normal functionnality, pressing A moves left, pressing D stops the character, and depending on whether A or D is released, the character will resume moving to the right or left.

With variant 2, we have the advantage of quickly changing direction. Strafe left, A, move right D, changed my mind, release D, go left.

In contrast, with variant 1, once the direction is changed, deactivating A or D forces the user to release the key before being able to use it again.

In terms of UX, variant 2 provides almost the vanilla experience but with an improvement.

Realistically speaking, it is very unlikely someone will swap from one variation to another, and it is simpler to activate the variation we want upon compilation of the firmware.

henrebotha commented 1 month ago

In fighting games, the setting that first emerged as de facto standard was in fact using two different algorithms for the two axes. The horizontal axis would produce a neutral output (i.e. no output when both left and right are pressed, "nullification" per the RFC), and the vertical axis would produce an up output (i.e. up always "wins"; "exclusion" per the RFC, but only ever yields up, never down). There are clear gameplay advantages inherent to this (up always taking priority aids in super jumps, down-up charge motions, and tiger knees, whereas sideways motion must not be biased in either direction).

Nowadays, another standard is also very common, which is both axes summing to neutral. This is currently enshrined in the competitive rules for the Street Fighter 6 Capcom Pro Tour, for example.

In all above cases, releasing the second key so that only the first is held would again produce the first key as output.

The Smash community, which is historically separate from the "traditional" fighting game community, has a slightly different approach. I believe some implementations there would behave exactly like "cancellation" per the RFC; this feature is sometimes called "2IP (second input priority) no reactivation".

I think it's useful for the conversation to distinguish between what happens at the moment the second key is pressed (do we emit only the second key? Or perhaps nothing at all?), and what happens when the second (or first!) key is released.

I would reference Hit Box Arcade's information pages on this topic, as they popularised the concept of SOCD cleaning and defined many terms that are in use today.

Switching at runtime is a relatively popular option. Even playing different characters in the same fighting game can benefit from different algorithms.

aldehir commented 1 month ago

I applied #24000 and played a few games of Overwatch. In my opinion, it was unplayable. In the chaos, I constantly found myself standing still since I was not reactivating a movement key after it was cancelled. This leads me to believe that I rely more on which keys I have pressed than the pressing action itself. It is a little jarring to have A pressed but not reported because it was cancelled by a brief press of D.

For this reason, I believe Variant 2 is the best for first person shooters.

On Variant 3, most games that utilize WASD movement will associate each key press individually with a velocity. If A is -600 units/s and D is +600 units/s, then they already cancel out and the game reports you as having a velocity of 0 units/s. I only have experience with the Unreal Engine, so I cannot say if this is strictly true for all games.

Personally, I would like the ability to turn key cancellation on/off at runtime, which #24000 already provides. I don't see the need to swap between variants, as I will always use Variant 2.

lukescott commented 1 month ago

How does Razor or Wooting do their implementation? For anyone looking to use this feature, they are going to be looking to match one of those two. So which ever option matches the functionality of those keyboards, that's the option you should go with. Not saying that it can't be improved on, but for a version 1 that should be the mvp.

Changes and improvements beyond the initial mvp should be considered after version 1.

CalmConcept commented 1 month ago

As far as I can tell, Variant 2 seems to be the most similar to Razer's Snap Tap.

tzarc commented 1 month ago

I’m hiding comments that don’t answer the questions posted in the description. Please stay relevant.

kqxu1017 commented 1 month ago

Mainly CS2, and also other fps player here. I believe that the main reason Razer released Snap Tap is for better (or quicker) counter-strafe in CS2 (as advertised on their website), becasue as far as I know, no other games (like Val) rely on counter-strafe as much as CS2 does.

In CS2, movement (by pressing AWSD) is an acceleration, and only firing under a certain speed is precise. The point for counter-strafe is to make the player slows down faster by giving it and opposite-direction acceleration. Therefore, a perfect counter-strafe would be the release of one key (for example, key A), and the press of the other (in this case, key D) happened at the same time, so there is always an acceleration input, just in opposite direction.

Snap Tap is helpful in CS2 because it eliminates the human error to make the counter-strafes stably perfect (which represents faster) By human error I mean when i try to release one key and press the other at the same time, there is actually a short period of time when both keys are being pressed or both are released (null input, which equals to 0 acceleration).

I believer what people are asking for here is something similar to Snap Tap or SOCD, which in my opinion, is what the variant 2 does.

For the variant 3, it does not make any change of behaviors in terms of CS2 playing, so for me, it is useless.

For the variant 1, theoretically it does the same thing in CS2, but what was mentioned in one of the previous comment makes me believe that it can actually increase human error in some cases.

In the chaos, I constantly found myself standing still since I was not reactivating a movement key after it was cancelled. This leads me to believe that I rely more on which keys I have pressed than the pressing action itself. It is a little jarring to have A pressed but not reported because it was cancelled by a brief press of D.

lukescott commented 1 month ago

@tzarc I'm sorry, but how is my comment off-topic? You asked about which implementation to go with. The implementation people care about is the one that matches what already exists. I asked how do the current options compare to existing implementations. Please explain.

Variant 2 seems to make the most sense from a usability standpoint. Especially when you think about it as 2 opposite directions cannot activated at the same time. With A held down, pressing D would be "last direction wins", but if D is released, A is still being pressed, so reactivating it makes sense from a user perspective. Variant 1 and 3 do not accomplish that. Or at least Variant 1 requires additional interaction by the user that is error prone and unnatural.

If Razer does Variant 2, that should be the one to implement, especially since they have already done the R&D. I think it's helpful to consider this when evaluating the options. It's also going to be what the user will expect when enabling the feature. This falls in line with POLS (principle of least surprise).

What we don't know is: Whether or not there's a necessity to mix and match at any single point in time:

  • Would a game be best served by having Cancellation on A/D, Nullification on W/S, or any other mixed use of variations?

W/S, or Up/Down, Forward/Back are not typically pressed at the same time. It's either one or the other. Finger position also doesn't really lend to pressing either of those at the same time. There is also a tendency for the user to hold down W (Forward) and sometimes occasionally press S (Backward). While W is being held down, A or D is pressed to offset forward momentum into a direction. This is pretty standard.

The only case I can think of that might want to include other directions is Osu!

Beyond that if we're talking about a moving character in a 3D environment, the use-case that is being targeted is cases where:

But beyond that, without considering how some of the implementations are configured, I can speak to only how I would use the feature.

Whether or not users are only ever going to want one active on a firmware:

  • Choose one of cancellation/exclusion/nullification at compile time
  • List all pairs like the PR does today, each pair will be subject to the original choice of mode

Whether or not users are going to want to swap them around at runtime:

  • Much like unicode modes, swapping from Win/Linux/macOS
  • Some modes may be relevant for different games, and swapping may be necessary.

I would say that if I were to use this feature, I would want a way to enable/disable a pre-configured combination of keys at run-time. I would setup A->D and D->A, but have it disabled at run-time most of the time for regular typing (writing code, browsing the web, etc). But then I would want to enable the feature at run-time when I am playing a particular game where A and D represent Left/Right directions.

I would see myself using Variant 2 only.

(In my case it would be S/F since I use ESDF as opposed to WASD)

Looking at the original PR, the configuration was something like [[A, D], [D, A]]. If you were to make a change, I think you could simply do [A, D] where [D, A] is implied. I think it's fairly safe to assume that:

You could ask "are there any use cases that involve more than 2 keys?". But I think we also run the risk of bike shedding if we go that deep. What prompted the feature in the first place was providing a feature users want by a competing product.

infinityis commented 1 month ago

If Razer does Variant 2, that should be the one to implement, especially since they have already done the R&D. I think it's helpful to consider this when evaluating the options. It's also going to be what the user will expect when enabling the feature. This falls in line with POLS (principle of least surprise).

I believe all the listed implementations are valid and applicable depending on the use case, so implementing just one doesn't seem to make sense. There might be a good argument as to which should be the default if it is left unspecified, however. I wouldn't put a lot of stock into Razer's R&D specifically, but instead look at what the most common implementation is.

What we don't know is: Whether or not there's a necessity to mix and match at any single point in time:

  • Would a game be best served by having Cancellation on A/D, Nullification on W/S, or any other mixed use of variations?

W/S, or Up/Down, Forward/Back are not typically pressed at the same time. It's either one or the other. Finger position also doesn't really lend to pressing either of those at the same time.

While this seems likely true for normal keyboard layouts, on a layout like the Razer Kitsune (where Up/Down can easily be pressed simultaneously), this observation appears to be inaccurate.

Whether or not users are only ever going to want one active on a firmware:

  • Choose one of cancellation/exclusion/nullification at compile time
  • List all pairs like the PR does today, each pair will be subject to the original choice of mode

Whether or not users are going to want to swap them around at runtime:

  • Much like unicode modes, swapping from Win/Linux/macOS
  • Some modes may be relevant for different games, and swapping may be necessary.

This setting should be swappable at runtime, and should have one setting that applies to the entire keyboard. Without even digging very deep, the very first customer review I read about the Razer Kitsune laments that you can't change the SOCD cleaning mode. Having the ability to globally and consistently change the modes at runtime seems like a valuable thing to do.

That said, so as to not be overly prescriptive (nor overly complicated), I also think that in code the mode setting should be stored and tracked per-pair, in addition to a mode setting for the keyboard. That allows user or keyboard code to override the global setting.

So for keyboards which implement this feature, there would be a keyboard-wide setting:

And for each pair of keys to which this capability applies, there would be a setting:

Only the keyboard-wide setting would be stored persistently via eeconfig; the per pair settings just provide a means for custom firmware to handle special use cases. For example if it is found that W/S handling needs SOCD cleaning yet it should be handled differently from the other keys in a given SOCD mode, that can be specified in some _kb or _km code that gets called upon a change in the settings. Given that W/S are often not typically pressed simultaneously due to traditional "same finger" placement, there may not be enough data yet to know if it makes sense to consistently use a different mode for different keys.

Regardless, implementing the firmware per-pair keeps the code simpler, with the only keyboard-wide code being storage/retrieval of the global setting, as well as a compile-time check to ensure that each key involved is exclusively paired up with only one other key (unless the implementation somehow supports one key being "paired" with multiple other keys...?).

The reason for a global setting is for user convenience. Being able to change the mode (e.g. with DIP switches or maybe a settings menu on a display) at runtime and get immediate confirmation (via display or LEDs) that the keyboard is in the desired mode seems like it would be tremendously useful.

tzarc commented 1 month ago

I believe all the listed implementations are valid and applicable depending on the use case,

Correct; this wasn't ever in question despite what other commenters have said. We're establishing the need for configurability and whether or not multiple simultaneous modes would be necessary.

This setting should be swappable at runtime, and should have one setting that applies to the entire keyboard.

I don't agree, personally. If we have per-pair config as you're suggesting, then I'd rather just have on/off keyboard-wide (potentially ee-saved), with the ability to override using a _user callback if necessary. If each pair has their own default config, it's picked up by default, and would allow people to build "profiles" for different games where different modes are active per-pair.

If you had something like:

typedef enum keymode_t {
    MODE_DISABLED,
    MODE_CANCELLATION,
    MODE_EXCLUSION,
    MODE_NULLIFICATION,
} keymode_t ;

typedef struct keyconfig_t {
    uint16_t keycode_a;
    uint16_t keycode_b;
    keymode_t mode;
} keyconfig_t;

// with a default config:
__attribute__((weak))
keymode_t get_keymode_user(uint16_t keycode_a, uint16_t keycode_b, keymode_t default_mode) {
    return default_mode;
}

// and keymap.c override:
keymode_t get_keymode_user(uint16_t keycode_a, uint16_t keycode_b, keymode_t default_mode) {
    if (strcmp(current_game, "Red Alert 3") == 0) { return MODE_EXCLUSION; }
    if (strcmp(current_game, "Jazz Jackrabbit") == 0) { return MODE_NULLIFICATION; }
    if (strcmp(current_game, "Kirby") == 0) { return MODE_DISABLED; }
    return default_mode;
}

...then people could override the default config at their leisure. Only constraint is that the pairs are defined in a table up-front.

Alternatively, keymap introspection overrides would be usable too. There's already an expectation that any keymap-defined tables have equivalent introspection APIs anyway.

infinityis commented 1 month ago

Your method of doing a _user override looks great, and simpler than what I was suggesting (especially since orverrides are not likely to be used enough to justify formalizing an implementation in core).

If the mode is only selectable as on/off at runtime, then referring to that Amazon comment I linked to previously, would the solution to enabling a different SOCD mode be "compile and flash new firmware with the desired mode enabled?"

Also, I was wondered about basing it on keycode pairs vs switch matrix location pairs earlier when writing my previous comment, but based on the function prototype you provided it looks like the assumption is that the SOCD mode is being applied to keycodes. Without having put much thought into it, it wasn't immediately obvious to me whether one approach is more "correct" than the other. Since the functionality related specifically to finger positioning allowing for simultaneous keypresses, I thought there may be a case to make the pairs tied to the hardware location (especially for a gaming pad where the finger placement is relatively fixed), but I can also see how keycodes can also work. I guess the question is whether it makes sense for key pairings to follow the hardware or follow the keycodes (or if it doesn't matter either way, in which case just choose with whichever is simpler to implement).

dexter93 commented 1 month ago

Feedback is looking good so far. Let me try to steer the RFC to something that hasn't been explored, but is crucial on implementation planning

What we don't know is:

  • Whether or not there's a necessity to mix and match at any single point in time:

    • Would a game be best served by having Cancellation on A/D, Nullification on W/S, or any other mixed use of variations?

keeping in mind, ofc

Realistic usage patterns, please.

The other aspects of this RFC have been pretty much covered by @tzarc in his suggested implementation. Do we need to go the extra mile for simultaneous active multi-mode? If so, please comment examples.

Psebcool commented 1 month ago

I can confirm that "Variant 2: exclusion" is the best option !

Venadux commented 4 weeks ago

To reply to the question about having mixed use of variations with A/D and W/S or other : as an avid fps game enjoyer (30h csgo, 300h valo, 400h overwatch, and more) I don't personally believe having two or more variations at the same time is necessary because I do not find myself pressing keys other than A/D at the same time. To go in more details, I use three fingers on my WASD keys one for the D key, one for the W/S key and the last one for the A key, which means I never press W/S at the same time unlike I do with A/D. So to resume, I don't think a mixed use of variations is useful in fps games because I never seem to press multiple keys at the same time except A/D. Hopefully this covers the question, but I would like to say that I can't speak for other genres of games like fighters for example so it would be good to get their opinion on this.

torecsgo commented 4 weeks ago

In my opinion (CS Player, reached some high level and talked with some pros). We think that the best is the second one that allows you to cancel A while pressing D but allows you to restore A if you kept pressing it and released D (and vice versa, with W/S same thing). I and some people in high level of CS we were talking that in this way you will get the Wooting/Razer idea (one of the guys that I was talking is using wooting keyboard).

I don't know if this helps you in the development of the idea, but all of this is in my opinion and after hearing some professional CS players.

Summary: Variation 2 (if I understood it correctly)

adrianFX commented 4 weeks ago

+1 for variant 2 Being able to hold a and spam d allows for the fastest strafes in games with instant movement acceleration(overwatch) therefore giving you the biggest competitive advantage. While games with more realistic movement acceleration like counter strike or valorant benefit from the fastest possible deceleration

tzarc commented 4 weeks ago

Locking this issue as recent responses seem to be unable to answer the questions posed.