-
Notifications
You must be signed in to change notification settings - Fork 47
Design Decisions
This is just some scratch space to write out some decisions, and possibly change them.
This is quite possibly a bad idea, but the goal is to have the event loop
epoll device node -> read node -> dispatch to translator -> write to virtual slot
be lock-free to avoid input latency. I have done no testing to see how much slow down introducing locks might bring, and so this might be a disaster without a good reason. It seems to be working fine though, as write calls are atomic. If you have some testing that even under a reasonable load the input delay is minor, we could save some headaches here.
It will be easier to transition from attempted lock-free to lock-ful than vice versa.
All other behavior is not latency critical, and should be appropriately synchronized. This includes adding/removing devices, changing profiles, parsing, and printing.
To support this, input_source has an internal pipe that allows the external interfaces to queue up messages. These messages are read by the device's thread that has the sole authority to update the various items used in the loop above, thus avoiding race conditions there.
Event translators are stored in profiles as objects able to clone themselves. This allows us to propagate copies that can be deconstructed independently I wasn't sure how else to handle this, as I didn't want device threads to handle parsing yet again. They aren't simply lambdas as some translators really could use the nice state keeping of having a true class. They also aren't just the strings used to specify them as it would be a pain to spread out and repeat that parsing step.
The main fallout of this decision I see is that there is a hard constraint that two events of different types (ABS vs. KEY vs. REL) cannot have the same name, as they'd have to somehow share a translator. That already-parsed translator might be specialized towards a particular input type.
The moltengamepad object is mainly concerned with storing smart pointers in lists: the managers, the profiles, the devices, some thread pointers, the udev and uinput objects, and the slot manager.
The options class is a glorified key-value store that takes all values as strings and can parse and retrieve values as their real representation. It also stores some metadata on the options. It has an optional callback field to be called whenever an option is given a new value.
The profile class is a more complicated key-value store encompassing all the things that can be changed at device-by-device level. It stores mappings from event names to event translators, event name lists to advanced translators, from external aliases to single events, from external aliases to event groups, and an options object.
Profiles can have subscribers, so that changes to one profile can propagate to others. The whole structure of these profile links should be a tree (or a forest). No cycles. Profiles can also have input_sources, which essentially act as subscribers.
For example, with a device "wm1" from the wiimote driver, the arrows indicate a subscription:
gamepad -> wiimote -> wm1 (profile object) -> wm1 (input_source object)
The gamepad profile acts as a root profile for all gamepad-esque drivers. By defining aliases, these gamepad mappings can be inherited sensibly. Ex. aliasing "primary" to "cc_a" means "gamepad.primary = ..." will have the desired effect on the wiimote profile as well.
The input_source object keeps its own copies of the event translators from the profile so that is may use a more convenient flat array structure.
Output is handled through message_streams, representing a "topic" of messages and a granularity to which clients can subscribe and listen for events. For remote clients, one request might get multiple responses or ongoing responses. To disambiguate, each request is given an id, and all output related to that request carries that id. The response_stream class is for convenience to hide that response id.
MG uses many threads.
- A main thread that does almost nothing but wait for a signal to quit and clean up everything.
- A udev thread waiting for udev events.
- A thread reading standard input (optional).
- A thread reading the FIFO (optional)
- A thread waiting for socket connections (optional)
- A thread for each socket connection (optional)
- A thread for each input_source
- A thread for reading incoming rumble events (optional)
- Some drivers might spin up their own thread if they don't use the udev one.