Code Monkey home page Code Monkey logo

Comments (11)

dmadison avatar dmadison commented on June 7, 2024

I could be wrong, but I'm not sure if you understand what you're asking.

On the PC side of the bus the XInput API is several levels removed from the hardware. The user does not request the state from the device itself, they request it from the driver's buffer. The driver receives periodic updates from the host USB controller, which itself requests and receives periodic updates from the device.

On the microcontroller side XInput works like any other USB device. The device is sampled at a fixed rate according to the configuration descriptors (bInterval) and packets are either sent or the request is NACK'd if there are no packets available. The library functions one level higher in the abstraction layer, as the hardware USB controller on the microcontroller handles the actual interface with the bus and transferring packets out of the controller's data buffers.

If you create a callback at the request time the USB hardware has already handled the request, because that's where the interrupt (callback) would be triggered from. Any data queued at that time would be for the next packet, and delayed by the request interval. That would in theory make updates more consistent, but at the expense of increased latency.

For the 32U4 in particular, take a look at the USB controller portion of the datasheet, section 22.14 ("IN endpoint management") (page 277).

It seems like this is necessary to reduce the lag of the controller and make it consistent

Are you experiencing noticeable latency or is this theoretical?

If you were instead sampling every 8 ms and sending that with the current API, your input delay could be 0-16 ms

I don't follow your math here. Packets are sent at the next request, which is always going to be at or less than the request interval. In your example with an 8 ms request interval, if you queue a packet to be sent (XInput.send()) immediately following an endpoint request, the latest the packet can be sent is at the next request (+ 8 ms).

The user's own sampling time doesn't factor into it - e.g. if the user samples once every hour the data is still sent within the request interval from when it's queued, so the "lag" is still 0 - 8 ms at the time it's sent. Any delays past that is on the user's own implementation.

from arduinoxinput.

clevijoki avatar clevijoki commented on June 7, 2024

This is all theoretical, for context I am trying to make a game controller so I am reading via digitalRead and passing it onto the XInput library.

Also I am completely new to this USB hardware thing and freely admit I may be asking for the impossible.

I don't follow your math here. Packets are sent at the next request, which is always going to be at or less than the request interval. In your example with an 8 ms request interval, if you queue a packet to be sent (XInput.send()) immediately following an endpoint request, the latest the packet can be sent is at the next request (+ 8 ms).

If I was to use a loop like:

void loop()
{
   XInput.setButton(BUTTON_A, digitalRead(1) == LOW);
   XInput.send();
   delay(4); 
}

This inherently adds 0-4 ms of latency from the time the button was pressed to the time it is read via digitalRead, and then additional latency from the time XInput.send() occurred vs it being read by the usb controller.

If I follow things correctly, bInterval is set to 4 ms (USB_XInput_Descriptors.cpp), so XInput.send() may have occurred immediately after it was sampled by the USB host controller, adding 4 ms of lag, or immediately before, adding 0.

So the lag here (on the arduino side) is 0-8 ms total.

If however, a callback could be provided when the packet was sent (again, is this even possible?) then I could alter the next delay() time to sync XInput.send() immediately before it was requested:

uint32_t g_SyncTimer = 0;
void justSent()
{
    g_SyncTimer = micros() + 3900;
}

void loop()
{
   delayMicroseconds(g_SyncTimer-micros());
   XInput.setButton(BUTTON_A, digitalRead(1) == LOW);
   XInput.send();
}

Then with the bugs fixed, lag should be reduced to be 0.1-4.1ms

Correct me if I'm way off base here

from arduinoxinput.

dmadison avatar dmadison commented on June 7, 2024

I don't think you're way off base, but I do think you're overthinking it.

If I was to use a loop like:
...
This inherently adds 0-4 ms of latency from the time... So the lag here (on the arduino side) is 0-8 ms total... Then with the bugs fixed, lag should be reduced to be 0.1-4.1ms

That's not a reduction, that's an increase. Without any delay at all the library is already at 0-4 ms latency. You do not need a delay in the main loop for the library's sake.

Data is only sent if a control surface changes, and XInput.send() blocks until the packet is cleared. You do not need to sample at the same interval as the library, you can sample as fast as your I/O interface allows you to. A packet will be queued as soon as the control surface changes and then is sent as soon as possible, which is typically what you want with a controller because it results in the most immediate data even if it's not technically the lowest latency.

(The exception to this rule is noise on the analog inputs, but the library can't do anything about that because it's hardware agnostic.)

That being said you should be able to (roughly) get the time when a packet is sent already - check whether the XInput.send() function returns an amount of data sent >0 and save a timestamp then.

from arduinoxinput.

clevijoki avatar clevijoki commented on June 7, 2024

In this example sure, but what I am actually trying to do (actually the whole point of my project) is to add a delay to be able to process the input, e.g. be able to press diagonals or button combos without a leading direction input occurring.

This delay is just way simpler to evaluate if I do it in blocks, and it makes sense then to synchronize those blocks to the packet send times. It will take some thinking about how to achieve this given the XInput.send() timestamp.

from arduinoxinput.

dmadison avatar dmadison commented on June 7, 2024

Sorry, but I'm afraid I'm not following what you're attempting to accomplish. My only advice is to bear in mind that with this library you are in total control over sending data as the device - no input goes out unless you tell it to.

from arduinoxinput.

clevijoki avatar clevijoki commented on June 7, 2024

So looking at how XInput.send() works, it seems like to actually reduce latency you have to write something like this:

void loop()
{
    uint32_t end_time = micros() + 3900;
    while (micros() < end_time)
    {
        XInput.setButton(BUTTON_A, digitalRead(1) == LOW);
        XInput.setButton(BUTTON_B, digitalRead(2) == LOW);
    }
    XInput.send();
}

Because XInput.send() blocks (only if something changed, which is also a bit of an issue) it naturally syncs to the next send frame.

If I instead did things naively:

void loop()
{
    XInput.setButton(BUTTON_A, digitalRead(1) == LOW);
    XInput.setButton(BUTTON_B, digitalRead(2) == LOW);
    XInput.send();
}

... and I attempted to press A and B at the same time, it would always end up blocking on XInput.send() and the second button would get deferred to the next XInput send block, which is 4 ms away. This is because it's impossible, as a human being, to press them exactly at the same time so there will be a gap between them that is longer than it takes digitalRead(1) to succeed.

If autoSend was on it seems like it would guarantee every input was sent 4 ms apart.

from arduinoxinput.

dmadison avatar dmadison commented on June 7, 2024

it seems like to actually reduce latency you have to write something like this:

Because it bears repeating: for most people and most games this is a non-issue. As you said you do not have the reaction time to press both buttons at the exact same instant and games, even games with tightly timed combos and button sequences, know this. The low level packets sent out by the device are not what the game acts on, the game acts on the current state of the gamepad as returned by the driver. Being separated by 4 milliseconds (or 8, or 12, or 16) only makes a difference if the game is processing the inputs at that same rate. Unlike most older game consoles the two aren't closely coupled and there's several layers of abstraction between them.

You can test this yourself for your application by creating a few artificial packets with varying delays between them and seeing how much of a time difference you can get away with. It's probably more than you think.

The other thing to bear in mind is that trying to time to the packet like that creates a race condition, where the code to read the inputs, write the (filtered) controls to the buffer, and transfer the buffer to the output must be completed before the USB request is received by the hardware controller. Otherwise the packet will be queued after the current request has been NACK'd and you'll end up sending frames every other update instead.

If autoSend was on it seems like it would guarantee every input was sent 4 ms apart.

If they change, yes. That's the intended behavior because it makes every input atomic.

If you want every update to block until packet send you can remove the newData check, but be forewarned that it'll send data constantly. That doesn't match the behavior of the real controller the library is emulating and might confuse the driver. YMMV.


To be honest if you're in the pursuit of zero input latency at all costs this is not the library you want - you would be much better suited with the generic Joystick library, which uses class-compliant HID and can run up to 1000 Hz without issues.

from arduinoxinput.

clevijoki avatar clevijoki commented on June 7, 2024

Thanks, I'll check it out.

This does have real world impacts on fighting games, where striving for clean inputs are part of the game, and it runs at a fixed 60hz and samples inputs once per frame.

I did do some tests here with Tekken 7, sending test messages tied to specific buttons to see how it processes input, and a leading 4ms packet will be read 24% of the time.

Your true failure rate should be related to how close your button presses are together, which can be imperceptibly close when doing actions like diagonals.

I'm not going for ultra low latency, I'm going for reliability, if you could press buttons < 4ms apart there should never be a leading single button frame.

from arduinoxinput.

dmadison avatar dmadison commented on June 7, 2024

it runs at a fixed 60hz and samples inputs once per frame.

In which case you should have a window of 4 updates per frame. Those inter-frame differences shouldn't matter unless your own timing is delayed.

I'm going for reliability, if you could press buttons < 4ms apart there should never be a leading single button frame.

Strictly speaking that's not true, it's in fact the opposite - if you press buttons > 4 ms apart they should always be split because that's the update interval. In timing intervals less than that it depends entirely on where the button inputs occurred relative to the update request. As in your example above: if you press buttons within < 1 ms of each-other but the first press occurs immediately before an update request the second press will get sent after the update. That's not due to the library that's just how the timing intervals work out. You can't predict that the user is going to press a second button until they do.

You cannot guarantee that non-simultaneous presses at less than the update rate will get sent in the same packet. The only way I can think of to guarantee that both presses at 2-3 ms apart get sent in the same packet is to synchronize your own human movement to the USB clock (haha). Or delay the entire packet waiting for an input that may or may not arrive.

Otherwise the best you can do is to send packets quickly enough that it doesn't matter, which is why I suggest trying out the 1 ms update rate of the Joystick library if the 4 ms here isn't cutting it.

Your idea to sample immediately before the next request is a good one if that's what your project requires, you just need to be careful to get all of the processing done so the packet is in the buffer before the USB controller comes to call.

from arduinoxinput.

clevijoki avatar clevijoki commented on June 7, 2024

In which case you should have a window of 4 updates per frame. Those inter-frame differences shouldn't matter unless your own timing is delayed.

They do matter though in some games, and I measured it as such in Tekken 7.

If you were to send two packets of info, BUTTON_A on, then BUTTON_A off, there is only a 24% chance that the game will register your input, because that is the chance that your 4ms of "on" state is active when the game samples (every 16.666 ms).

If you were to send a 5 packet stream of A,B,B,B,B, which represent 20 ms of time, most of the time you will get a "B" input, sometimes an "A" followed by "B" input, and very rarely, just an "A" input. Again, this is not hypothetical, this is what I measured in game.

Strictly speaking that's not true, it's in fact the opposite - if you press buttons > 4 ms apart they should always be split because that's the update interval.

That is true, that is why I said < 4ms.

The only way I can think of to guarantee that both presses at 2-3 ms apart get sent in the same packet is to synchronize your own human movement to the USB clock (haha).

This is what my delay buffer is for, if you know the "future" you can discard extraneous inputs

from arduinoxinput.

dmadison avatar dmadison commented on June 7, 2024

If you were to send two packets of info, BUTTON_A on, then BUTTON_A off, there is only a 24% chance that the game will register your input

Naturally, because you changed inputs before the game could measure it. That's one of the reasons why debouncing exists.

That is true, that is why I said < 4ms.

My point is that that's incorrect. Two buttons pressed at an interval greater than the update rate will always be split. Two buttons pressed at an interval less than the update rate may or may not be split dependent on when the update occurs. At any press interval less than the update rate you cannot guarantee that the two inputs will be grouped together in a single update.

This is what my delay buffer is for, if you know the "future" you can discard extraneous inputs

True! Although that's at the expensive of input latency, since immediate inputs would need to be delayed at least one update if not more to check if a matching complement is also pressed.

I personally don't play Tekken so I'll leave the specific control variants and predictions to your expertise.


We've strayed a ways off topic from the original issue so I'm going to close the discussion for now. Best of luck with the project!

from arduinoxinput.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.