input-remapper icon indicating copy to clipboard operation
input-remapper copied to clipboard

Wheel events not smooth

Open marcsabat opened this issue 3 years ago โ€ข 30 comments

I mapped the left/right scroll buttons to the scroll wheel events and it works. That is: while I keep clicking the mouse wheel to the left it scrolls up (insted of horizontal scroll) and viceversa.

However scroll is not smooth, not "continuous". Mapping the button to wheel(up, 1) sends a scroll event once every second. However, if I map the button to higher speeds such as wheel(up, 50) the resulting scroll is not smooth. Feels like some events are being discarded.

I see this in macro.py (line 495 at the time of writing, add_wheel function): await asyncio.sleep(1 / resolved_speed)

This should send and mouse wheel event regularly but it seems it "misses" somtimes.

Could it be because of the "debounce" feature comes into play?

Note: this behaviour is annoying because it causes a bit of dizziness when scrolling due to its impredictible nature.

marcsabat avatar Mar 25 '22 10:03 marcsabat

the debouncing is for mapping a wheel to a key.

you are doing it vice versa: mapping a key to the wheel

does changing line 492 to handler(EV_REL, code, value * 2) help? If yes, then the wheel macro should probably get a second parameter to add a factor for the value.

sezanzeb avatar Mar 25 '22 10:03 sezanzeb

@sezanzeb I think this is due to the use of REL_WHEEL and REL_HWHEEL in the add_wheel() function. According to the linux kernel documentation:

These event codes are legacy codes and REL_WHEEL_HI_RES and REL_HWHEEL_HI_RES should be preferred where available

Using the _HI_RES event codes in the macro and adjusting the default speed should give a much smoother experience.

jonasBoss avatar Mar 25 '22 12:03 jonasBoss

I don't think this is the issue here.

If I scan the code send by the mouse in a regular wheel scrolling (using evtest) it sends either values 1 o -1. If I try to be "smooth" with the actual wheel it works as expected. When mapping the button to wheel events the scroll pauses 2 o 3 times each second.

Some additional info:

  • The same happens if the speed value is high (i.e. 1000): scrolls really fast, brief pause, scrolls really fast, brief pause, etc. It seems it sends a bunch of events and then something stalls the process briefly until it resumes.
  • This is under Wayland.
  • I got used to this behaviour because the last mouse I had (Razer Basilisk v2) did, in fact, send multiple REL_WHEEL events (instead of REL_HWHEEL events) while pressing the scroll wheel left/right. I found that convenient. My new mouse sends REL_HWHEEL (as it should).

Missatge de Tobi @.***> del dia dv., 25 de marรง 2022 a les 11:42:

the debouncing is for mapping a wheel to a key.

you are doing it vice versa: mapping a key to the wheel

does changing line 492 to handler(EV_REL, code, value * 2) help? If yes, then the wheel macro should probably get a second parameter to add a factor for the value.

โ€” Reply to this email directly, view it on GitHub https://github.com/sezanzeb/input-remapper/issues/354#issuecomment-1078891899, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACSI3DY6HLZ2VLHM6LJGIATVBWKAJANCNFSM5RTY6VGA . You are receiving this because you authored the thread.Message ID: @.***>

-- Marc Sร bat

marcsabat avatar Mar 25 '22 12:03 marcsabat

REL_WHEEL can also have higher or lower values it seems. I would be interested in knowing if multiplying the values and sending less events improves it.

Other than that, we should probably also inject _HI_RES events. Thanks for pointing that out

sezanzeb avatar Mar 25 '22 15:03 sezanzeb

Ok, I tried both increasing the value and sending hi res events as well with the following results:

Increase the value

Everytime a scroll event is sent with a value higher than 1 a bigger portion of the screen scrolls up/down the screen. The jitterness, however, remains.

Send both regular events and hi res events

Kind of worsens the situation. Feels even less responsive.

I modified the code at macro.py as follows (REL_WHEEL_HI_RES and REL_HWHEEL_HI_RES need to be imported from evcodes):

codes, value = {
            "up": ([REL_WHEEL, REL_WHEEL_HI_RES], 1),
            "down": ([REL_WHEEL, REL_WHEEL_HI_RES], -1),
            "left": ([REL_HWHEEL, REL_HWHEEL_HI_RES], 1),
            "right": ([REL_HWHEEL, REL_HWHEEL_HI_RES], -1),
        }[direction.lower()]

        async def task(handler):
            resolved_speed = _resolve(speed, [int])
            while self.is_holding():
                for code in codes:
                    handler(EV_REL, code, value)
                
                # Value test
                #handler(EV_REL, codes[1], value * 4)
                
                # scrolling moves much faster than mouse, so this
                # waits between injections instead to make it slower
                await asyncio.sleep(1 / resolved_speed)

BTW, just sending hi res options does nothing (perhaps not yet supported?).

I have 3 theories:

  1. Using await asyncio.sleep is not consistent enough with the timings (I also tried to give the process a higher priority with no avail).
  2. Sending the event itself takes more time than the "speed" parameter. Using speed=1 works well: one scroll movement every second. Faster, usable speeds show the non smooth behaviour. Sending two codes (lowres and hires) makes things worse so this could be the problem.
  3. I can see many async operations and waits: maybe some waits are stalling the process.

Any other ideas I might try to diagnose the issue?

marcsabat avatar Mar 25 '22 16:03 marcsabat

Thanks for your tests!

Any other ideas I might try to diagnose the issue?

Yeah, it would be interesting to know if the wheel events appear at a consistent speed in sudo evtest

sezanzeb avatar Mar 25 '22 18:03 sezanzeb

Using evtest exhibits the same behaviour and I can actually put some numbers on it: image

The right column is the difference in time from last event. See the values marked in red: there are like 2 o 3 events sent on time and then a "long" wait (i.e. > 0,1)

marcsabat avatar Mar 25 '22 18:03 marcsabat

awsome, thanks! And is the same behavior visible in sudo input-remapper-service -d?

sezanzeb avatar Mar 25 '22 19:03 sezanzeb

@sezanzeb I didn't start the service the way you described at first. Once I did that I understood the problem. This is the output:

21:25:02.631499 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.650088 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.669381 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.689375 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.696581 161515 Service DEBUG keycode_mapper.py:430: releasing macro ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท ((2, 6, 0))
21:25:02.696822 161515 Service DEBUG keycode_mapper.py:443: releasing key ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท ((2, 6, 0))
21:25:02.829208 161515 Service DEBUG keycode_mapper.py:515: maps to macro (wheel(up,55), mouse) ((2, 6, -1))
21:25:02.829981 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.849109 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.868214 161515 Service DEBUG keycode_mapper.py:322: Macro sending (2, 8, 1) to mouse
21:25:02.887070 161515 Service DEBUG keycode_mapper.py:430: releasing macro ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท ((2, 6, 0))
21:25:02.887422 161515 Service DEBUG keycode_mapper.py:443: releasing key ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท ((2, 6, 0))

Seeing this the jitter comes from the fact that, despite keeping the button pressed, the mouse automatically releases and presses the key by itself for horizontal scrolling.

I checked with evtest (without key mapper daemon running and keeping the key pressed) and this is what's going on:

Event: time 1648240002.333769, -------------- SYN_REPORT ------------
Event: time 1648240002.533775, type 2 (EV_REL), code 6 (REL_HWHEEL), value 1
Event: time 1648240002.533775, type 2 (EV_REL), code 12 (REL_HWHEEL_HI_RES), value 120
Event: time 1648240002.533775, -------------- SYN_REPORT ------------
Event: time 1648240002.733790, type 2 (EV_REL), code 6 (REL_HWHEEL), value 1
Event: time 1648240002.733790, type 2 (EV_REL), code 12 (REL_HWHEEL_HI_RES), value 120
Event: time 1648240002.733790, -------------- SYN_REPORT ------------
Event: time 1648240002.933814, type 2 (EV_REL), code 6 (REL_HWHEEL), value 1
Event: time 1648240002.933814, type 2 (EV_REL), code 12 (REL_HWHEEL_HI_RES), value 120

I tried with a different mouse that happens to have horizontal scroll buttons and it works in the same way and, seems, at more or less same intervals for key press/release.

Finally I mapped the wheel to the mouse side buttons and it works as expected: smoothly.

So, mistery solved: the horizontal scroll buttons trigger multiple press/release events by itself while pressed. I believe that I cannot really do the mapping with horizontal scroll buttons the way I expected. I will keep the side buttons mapped and see if I get used to them.

Thanks for your help. Wonderful software.

marcsabat avatar Mar 25 '22 20:03 marcsabat

I mapped the left/right scroll buttons to the scroll wheel events

Oops I missed that somehow. Maybe the debouncing is broken after all like you suggested already

sezanzeb avatar Mar 25 '22 20:03 sezanzeb

I mapped the left/right scroll buttons to the scroll wheel events

Oops I missed that somehow. Maybe the debouncing is broken after all like you suggested already

Nah, I think you missed my last post. The mistery seems to be solved.

marcsabat avatar Mar 25 '22 20:03 marcsabat

I tried with a different mouse that happens to have horizontal scroll buttons and it works in the same way and, seems, at more or less same intervals for key press/release.

You mapped the horizontal scrolling to vertical scrolling, right?

sezanzeb avatar Mar 25 '22 20:03 sezanzeb

I tried with a different mouse that happens to have horizontal scroll buttons and it works in the same way and, seems, at more or less same intervals for key press/release.

You mapped the horizontal scrolling to vertical scrolling, right?

Yes. To be precise I mapped the buttons triggered by tilting the scroll wheel left or right.

Note for people trying with their own mouse and thinking "what the hell is that guy talking about": not all mice have this feature.

marcsabat avatar Mar 25 '22 20:03 marcsabat

If you observe the original wheel events from the mouse you'll probably see that REL_HWHEEL events are being injected, but no release events.

The debouncing is needed to treat the "button" as released when no REL_HWHEEL event arrives anymore.

If it releases randomly, then something in the debouncing is broken

sezanzeb avatar Mar 25 '22 20:03 sezanzeb

If you observe the original wheel events from the mouse you'll probably see that REL_HWHEEL events are being injected, but no release events.

The debouncing is needed to treat the "button" as released when no REL_HWHEEL event arrives anymore.

If it releases randomly, then something in the debouncing is broken

I am not sure about that because when releasing the button it does not emit any other signal (at least checking with evtest). Regular buttons, however, do send a 1 when pressed and a 0 when released.

I think it's just the way horizontal scroll is supposed to work. I don't know if this is per firmware or the way linux drivers work.

If key release comes actually from debounce perhaps being able to give more time for the debounce algorithm to consider the key to be released could solve the problem.

marcsabat avatar Mar 25 '22 21:03 marcsabat

If key release comes actually from debounce perhaps being able to give more time for the debounce algorithm to consider the key to be released could solve the problem.

this, or debouncing is buggy

sezanzeb avatar Mar 26 '22 00:03 sezanzeb

Check out https://github.com/sezanzeb/input-remapper/blob/main/inputremapper/injection/consumers/keycode_mapper.py#L289, it says ticks=3. Can you please try to increase this to, idk, 6 and see what happens?

sezanzeb avatar Mar 26 '22 00:03 sezanzeb

Check out https://github.com/sezanzeb/input-remapper/blob/main/inputremapper/injection/consumers/keycode_mapper.py#L289, it says ticks=3. Can you please try to increase this to, idk, 6 and see what happens?

I already tried that (even with much higher values) but I don't think it reaches this part of code since I added a debug message at that point and it won't show on the logs.

marcsabat avatar Mar 26 '22 07:03 marcsabat

Then it would be interesting to find out why it is not reaching this. If a wheel is mapped (to wheel(...), keys, etc.), then will_report_key_up should be False

sezanzeb avatar Mar 26 '22 08:03 sezanzeb

Good news: I had 2 copies of the source code and was modifying the wrong one. That's why my debug messages were'nt showing.

Long story short: it was actually the debounce function happening too soon for my use case. I modified the wheel macro to accept an optional parameter "ticks" and store it on the macro object (file macro.py):

def add_wheel(self, direction, speed, ticks=3):
        """Move the scroll wheel."""
        _type_check(direction, [str], "wheel", 1)
        speed = _type_check(speed, [int], "wheel", 2)
        self.ticks = _type_check(ticks, [int], "wheel", 3)

I added this to the Macro class init as well: self.ticks = 3

Then at keycode_mapper.py, instead of defaulting to 3 ticks, I did this:

self.debounce(
      debounce_id=(event.type, event.code, action),
      func=self.handle_keycode,
      args=(release, RELEASE, False),
      ticks=macro.ticks,
  )

This seems to do the trick! In my system I need to use about 8 ticks to make it work smoothly. So, inside the GUI, I defined the macro function as: wheel(up, 35, 8)

I don't know how to send a patch but changes are trivial enough. Altough my case is, perhaps, quite unique I think it would be useful to have the ticks variable exposed to functions for some other use cases.

marcsabat avatar Mar 26 '22 12:03 marcsabat

nice.

this would affect all other mappings as well. For example "wheel right" to "KEY_A". It would have to be added to all macro functions therefore and isn't specific to add_wheel.

Instead I'd rather have this exposed as a configuration value, just like keystroke_sleep_ms (or whatever the exact name was). I would call it wheel_debounce_release_ticks.

@jonasBoss would this approach cause problems with your changes? Would wheel_debounce_release_ticks become a configuration value for specific mappings within a preset? Is the debounce functionality for wheel releases still a thing?

sezanzeb avatar Mar 26 '22 12:03 sezanzeb

@marcsabat I am currently working on a new Injection system which has a setting that should address your problem. If you feel adventurous It would be helpful if you can test it with your mouse:

  • First a warning: backup all your Presets.
  • Then you can install https://github.com/sezanzeb/input-remapper/pull/263
  • Start the GUI, it will convert all your presets. The GUI is not very helpful and broken at the moment.
  • You need to edit the preset by hand for now.
  • open your mouse preset and find the mappings which map the mouse button to the scroll wheel. It should look something like this:
     {
         "2,6,-1": {
             "target_uinput": "mouse",
             "output_symbol": "wheel(up, 1)"
         },
         "2,6,1": {
             "target_uinput": "mouse",
             "output_symbol": "wheel(down, 1)"
         }
     }
    
  • edit the both mappings and add the line "release_timeout": 0.1 like so:
     {
         "2,6,-1": {
             "target_uinput": "mouse",
             "output_symbol": "wheel(up, 1)",
             "release_timeout": 0.1
         },
         "2,6,1": {
             "target_uinput": "mouse",
             "output_symbol": "wheel(down, 1)",
             "release_timeout": 0.1
         }
     }
    
    Note: the the placement of colons , is important to not invalidate the json syntax
  • This lets the injection wait longer for events before it stops scrolling (0.05 is default). you can increase the timeout if you still observe the macro stopping.

you can play around with two parameters:

  • the macro parameter for the speed
    • the speed will change the value that is sent with each WHEEL_HI_RES event (step size)
  • another line you can add to the mappings: "rate": 100
    • the rate will change how often the event is sent (steps per second 60 is default)

jonasBoss avatar Mar 26 '22 15:03 jonasBoss

@sezanzeb

@jonasBoss would this approach cause problems with your changes? Would wheel_debounce_release_ticks become a configuration value for specific mappings within a preset? Is the debounce functionality for wheel releases still a thing?

have a look at 82c8e64b2105ddb7dcd4f8ea57d6bce9fa02403d and my previous answer. We already have the solution in the new injection system.

jonasBoss avatar Mar 26 '22 15:03 jonasBoss

I tried the mentioned branch. At first it didn't work: it seemed to capture the events but won't send the new ones.

Then I looked at macro.py add_wheel function and realised that only hi res events were sent. When I changed the events to the regular ones (i.e. non high res) it worked again.

When looking at evtest output for the horizontal or vertical scroll wheels (without input mapper) I saw that both regular and hi res events are sent. Also, as per documentation, it seems that 1 click in regular resolution = 120 clicks for hi res. So, I changed the code for the add_wheel function as follows to send both hi res and regular wheel events:

def add_wheel(self, direction, speed):
        """Move the scroll wheel."""
        _type_check(direction, [str], "wheel", 1)
        speed = _type_check(speed, [int], "wheel", 2)

        codes, values = {
            "up": ([REL_WHEEL, REL_WHEEL_HI_RES], [1, 120]),
            "down": ([REL_WHEEL, REL_WHEEL_HI_RES], [-1, -120]),
            "left": ([REL_HWHEEL, REL_HWHEEL_HI_RES], [1, 120]),
            "right": ([REL_HWHEEL, REL_HWHEEL_HI_RES], [-1, -120]),
        }[direction.lower()]

        async def task(handler):
            resolved_speed = _resolve(speed, [int])
            while self.is_holding():
                for i in range(0, 1):
                    handler(EV_REL, codes[i], values[i]*resolved_speed)
                await asyncio.sleep(1 / self.mapping.rate)

        self.tasks.append(task)

This seems to work under Wayland in Manjaro Linux under kernel 5.16.14.

As you said I could control the release timeout. For my particular case about 0.13 works well.

marcsabat avatar Mar 26 '22 16:03 marcsabat

Thank you, that is good to know. So we do need the regular REL_WHEEL events as well. I was wondering that because on my pc (X11) it works without those.

jonasBoss avatar Mar 26 '22 17:03 jonasBoss

When looking at evtest output for the horizontal or vertical scroll wheels (without input mapper) I saw that both regular and hi res events are sent.

I think you want to add a mapping for REL_HWEEL_HI_RES -> disable so that those events are no longer forwarded.

If my guess is correct you are currently mapping the REL_HWHEEL -> REL_WHEEL + REL_WHEEL_HI_RES while the original REL_HWHEEL_HI_RES are untouched and forwarded. You might end up scrolling diagonal in some programs.

jonasBoss avatar Mar 26 '22 17:03 jonasBoss

Maybe. But so far in the tests I conducted with Chrome and Kate on pages that can scroll both horizontally and vertically everything works as expected.

One note regarding the speed parameter: in the main branch the speed controls the interval in which new events are emitted. In the new branch the speed controls the scroll length. While this works setting speed to 1 for this use case it's a tad too fast. I guess there's another parameter to control the rate at which the events are sent but, if I'm not mistaken, changing the value of this parameter would affect all the mappings. Is that correct or can it be configured per mapping?

Missatge de jonasBoss @.***> del dia ds., 26 de marรง 2022 a les 18:35:

When looking at evtest output for the horizontal or vertical scroll wheels (without input mapper) I saw that both regular and hi res events are sent.

I think you want to add a mapping for REL_HWEEL_HI_RES -> disable so that those events are no longer forwarded.

If my guess is correct you are currently mapping the REL_HWHEEL -> REL_WHEEL + REL_WHEEL_HI_RES while the original REL_HWHEEL_HI_RES are untouched and forwarded. You might end up scrolling diagonal in some programs.

โ€” Reply to this email directly, view it on GitHub https://github.com/sezanzeb/input-remapper/issues/354#issuecomment-1079739887, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACSI3D6MJIEW2HV5TB7KFUTVB5DFZANCNFSM5RTY6VGA . You are receiving this because you were mentioned.Message ID: @.***>

-- Marc Sร bat

marcsabat avatar Mar 26 '22 17:03 marcsabat

there is a setting for the rate which is independent for each mapping, you can add the line "rate": 30 to each mapping in the preset json file. I also just pushed a commit which should address the problem.

jonasBoss avatar Mar 26 '22 19:03 jonasBoss

the HI_RES events not working seems to be a Wayland issue there is already a merge request for that: https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/72

jonasBoss avatar Mar 26 '22 19:03 jonasBoss

I tried the new changes and work pretty well for this use case. Now the speed can be controlled better. I understand that the old macro wheel(up, 1) now should be written as wheel(up, 120).

Actually wheel(up, 1) does not work (speed value is too low). I guess that the usage documentation should be updated as well.

Thanks for all your excellent work and help!

Missatge de jonasBoss @.***> del dia ds., 26 de marรง 2022 a les 20:26:

the HI_RES events not working seems to be a Wayland issue there is already a merge request for that: https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/72

โ€” Reply to this email directly, view it on GitHub https://github.com/sezanzeb/input-remapper/issues/354#issuecomment-1079759457, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACSI3DYMX57RHSH5IVJCFQTVB5QGPANCNFSM5RTY6VGA . You are receiving this because you were mentioned.Message ID: @.***>

-- Marc Sร bat

marcsabat avatar Mar 27 '22 16:03 marcsabat