Naxdy
03d5c478ef
All checks were successful
Publish nightly release / build (push) Successful in 2m6s
|
||
---|---|---|
.forgejo/workflows | ||
src | ||
.envrc | ||
.gitignore | ||
build.rs | ||
Cargo.lock | ||
Cargo.toml | ||
Embed.toml | ||
flake.lock | ||
flake.nix | ||
memory.x | ||
README.md |
NaxGCC
This repo houses the firmware for the NaxGCC, a GameCube-style controller built on the PhobGCC. The firmware can also be used as an optional firmware for the PhobGCC, though the PhobGCC will then have to be connected to the console directly via USB.
Like the PhobGCC, the NaxGCC uses hall effect sensors instead of potentiometers for stick input. Additionally, it connects directly to the console via USB, by pretending to be a GCC adapter with 1 controller (itself) connected. This eliminates one additional layer of polling, and thus reduces perceived latency and improves input consistency. The NaxGCC firmware makes use of the embassy-rs framework for asynchronous operations. Mainly, this means that the firmware is capable of polling the sticks and buttons at different frequencies, further improving input consistency and latency for button inputs.
What's the deal with polling?
General Info
The vast majority of human interface devices (HIDs) transmit their states using a technique known as "polling". Essentially, these devices (clients) advertise themselves as supporting a certain polling frequency to whatever device they're attached to (hosts). The host then polls the client at the desired frequency, or at a lower one (= not as often, = more slowly) if the host doesn't support the client's desired frequency.
The Nintendo Switch supports polling USB devices at up to 125Hz, or once every 8ms. A game running at 60fps takes ~16.66ms to draw a frame, meaning with a polling rate of 8ms, a USB device would be able to update its state up to 3 times per frame. Notice how I said "up to", because this is where things start to get wonky.
The reason it's "up to 3 times" and not "3 times, period" is because the USB polling and the game's frame draws are not in sync. Otherwise, a polling rate of ~16.66ms would be perfectly sufficient, provided the host polls the adapter right before the frame is supposed to be drawn. Due to technical reasons, this is not possible however, so we are stuck with two asynchronous intervals: Polling & frame draws.
Input Integrity
Now, what does this mean for your input integrity? Essentially, the time window in which your inputs are guaranteed to come out on the frame you'd expect them to is equal to T_f-T_p
where T_f
is the time it takes to draw a frame (~16.66ms) and T_p
is the polling interval (8ms). So, on the Nintendo Switch, where USB polling is locked 8ms and frame time is 16.66ms, our window of time during which inputs are guaranteed to come out on the expected frame is 16.\overline{6} - 8 = 8.\overline{6}ms
This then means that during the first ~8.66ms of a "frame capture window", your input is guaranteed to come out on the frame you'd expect (the one whose frame capture window is currently open). For any input, the probability p(n)
that your input will arrive at the expected frame, for any n
that is the time elapsed (in ms) since the start of the frame capture window, is p(n) = \frac{T_f - n}{T_p}
. As you can see, for n \leq T_f - T_p, p(n) \geq 1 \rightarrow p(n) = 1
, and for n \gt T_f - T_p, p(n) \lt 1
.
In plain English, with a game running at 60fps and USB polling at 8ms, you have an ~8.66ms (slightly more than half a frame) window where your inputs are guaranteed to be consistent, everything outside that window is RNG to varying degrees, whether your input will be delayed by a frame or not.
Now this sounds kind of bad at first, but keep in mind that for the first few milliseconds before and after this "golden" window, the likelihood is still very high (\geq 75\%
) that your input will arrive at the frame you intended it for, so in total you will have a \approx 12.66
ms window where your inputs can reasonably be assumed to arrive at the frame you intended (sampling a random point from this 12.66ms window has a chance of \gt 92\%
of landing on the correct frame).
In reality, there will still be a little bit of RNG, and you won't be able to eliminate it fully, at least not with 8ms polling, which unfortunately is a limitation on the console side, but more than half a frame of guaranteed input integrity, and ~12.66ms of "reasonable" input integrity is something that I, as a competitor, could live with, if I had to. (spoiler alert: I don't have to!)
However, it's not that simple...
Joybus
See, the math above assumes that the GCC adapter is the device providing the inputs to the console. However, it is only a middleware, and the true source of your inputs is your controller. The GameCube controller interacts with the GCC adapter pretty much the same as with the OG GameCube, using the joybus protocol, which is the same protocol that N64 controller use. And its age shows, it doesn't have differential signalling, checksumming, or any of the other goodies other, more modern protocols (like USB) have.
But worst of all, it comes with yet another polling rate, one of 6ms. Now, you might be thinking to yourself "but Naxdy, 6ms is less than 8ms, so surely this is a good thing?" well yes, but no. It would be a good thing, if the GCC could be connected to the Nintendo Switch directly, but it cannot, it has to go through the adapter, which has its own polling rate of 8ms.
So, you end up with a system with three independent polling rates: The Switch polls the adapter at 8ms intervals, and the adapter polls the controller at 6ms intervals, and then the controller has whatever scan rate it has (Phobs have a 1ms scan interval FYI). Now, remember how the time frame in which your input is guaranteed to come out on the expected frame was T_f - T_p
, but what about a system with multiple polling rates? Well, in this case it's T_f - \sum_{i=1}^nT_i
where n
is the number of individual polling rates and T_1, T_2, ..., T_n
are the individual polling rates (in ms).
Again in plain English, if you have multiple polling rates, the frame window in which your inputs are guaranteed to come out on the frame you'd expect them to, is the total time of the frame window minus the sum of all polling rates. So, let's do some addition and subtraction for our use case here: 16.\overline{6} - 8 - 6 - 1 = 1.\overline{6}
. Now, you don't have to be a Harvard graduate to recognize that ~1.66ms is a teeny tiny window of time compared to the (theoretically perfect) ~8.66ms from before.
This is how it is when you're playing with a PhobGCC (the best GCC currently available) on a first party GCC adapter from Nintendo. Note that third party adapters may very well be much worse than this, because they could poll the GCC at an even lower frequency (= more slowly).
BONUS QUESTION: What if the sum of all polling rates is larger than the frame time window, i.e. \sum_{i=1}^nT_i > T_f
? That's right, in this case your inputs are always RNG! (good thing that's not the case here though)
The Solution
Since the NaxGCC connects directly to the console and eliminates the joybus protocol entirely, there is no second polling rate. The scan rate of the NaxGCC's sticks is 1ms, and the buttons are scanned as quickly as the MCU allows (I've measured ~50s on average, worst outliers being ~100us). While not quite reaching the ~8.66ms window length, the sticks have a ~7.66ms window of guaranteed input integrity, and the buttons are getting fairly close to ~8.56ms at worst (more than half a frame).
This isn't the end of it though. NaxGCC connecting directly to the console brings another advantage with it, namely we can "trick" the console into sampling the controller at a different interval, one that works to our advantage. We can actually pretend to be a "laggy" USB device, by artificially introducing a variable delay in order to ensure the controller actually sends its button state to the console every 8.33ms instead of every 8ms. Why 8.33? Because 8.33 is a multiple of 16.66 (the game's frame draw time), meaning the controller will be polled equally as often as the game updates. This ensures that if you press and release a button 100ms apart, it will always translate to 6 frames in-game (with polling at 8ms, there is a very high chance of it registering as 5 or 7 frames instead!).
The Experiment
So, what real-world impact does this have? I created a test in which I have the NaxGCC press and release a button, both in 100ms intervals. Meaning the game should register 6 frames held, 6 frames released, 6 frames held, and so on. I used the training mode modpack to measure the exact number of frames the game actually recognizes the button as held / released. I let this test run for 10 minutes and recorded it to a video, then used a python script to go through every 6th frame and record what the game actually registered using optical character recognition.
When in "OG controller" mode, the inputs were about ~75.25% accurate, meaning over 24% of the time, the game registered 5 or 7 frames held/released when it should have been 6. In "input consistency" mode, the accuracy was at ~98.62%! So, with a NaxGCC, not only do you get the lowest latency possible, since you're eliminating any sort of middleware in form of an adapter, but you also get the highest input integrity possible.