chore: update readme

This commit is contained in:
Naxdy 2024-04-02 13:36:18 +02:00 committed by Naxdy
parent c7fd7e72c9
commit 6c7ad81b80
Signed by untrusted user: Naxdy
GPG key ID: CC15075846BCE91B

View file

@ -4,52 +4,37 @@ This repo houses the firmware for the NaxGCC, a GameCube-style controller built
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](https://github.com/embassy-rs/embassy) 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. 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](https://github.com/embassy-rs/embassy) 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? ### Key Aspects
### General Info Click on any of these to expand.
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. <details><summary>NaxGCC has all the important PhobGCC features.</summary>
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 hardware of NaxGCC is directly forked from PhobGCC's, meaning it benefits from the same improvements over a "regular" GCC, most importantly the fact that it uses hall-effect sensors instead of potentiometers for reading your stick positions.
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. Furthermore, large parts of its firmware have also been taken from PhobGCC's firmware, such as the snapback filter, cardinal snapping, and notch remapping to name a few. If you're used to calibrating a PhobGCC, you will have no trouble here.
### Input Integrity </details>
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$ <details><summary>Firmware is written in Rust, using the [embassy-rs]() framework for asynchronous operations.</summary>
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$. The firmware being written in Rust allows for writing much cleaner code than one would normally be used to when writing firmware in C, because Rust allows for many zero and low cost abstractions in order to enhance code readability and maintainability. Adding embassy-rs for asynchronous operations on top of that provides 2 main benefits:
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. 1. It further improves code readability and maintainability by allowing to separate functionality on a semantic level.
2. It allows multiple tasks to be executed on the same thread, sharing their workload. Effectively, due to this, the NaxGCC can update its buttons at a ~50us (that's *micro*seconds) interval, and its sticks at a 1ms interval.
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). </details>
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!) <details><summary>Provides both the lowest latency of any Switch controller, as well as the best input integrity.</summary>
However, it's not that simple... </details>
### Joybus ## Contributing
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](https://www.int03.co.uk/crema/hardware/gamecube/gc-control.html) 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. The NaxGCC firmware is built using [nix](), which also provides a ready-to-go development environment, complete with all the tooling and libraries you need to get going. Simply install nix, [enable flakes]() and run
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. ```bash
nix develop .
```
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). and you're ready to work on the project. Submit your pull requests [here](https://git.naxdy.org/NaxdyOrg/NaxGCC-FW/pulls).
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](https://github.com/jugeeya/UltimateTrainingModpack) 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.