rust3ds / ctru-rs

Rust wrapper for libctru
https://rust3ds.github.io/ctru-rs/
Other
117 stars 17 forks source link

Add an ir:USER service wrapper and Circle Pad Pro example #86

Closed AzureMarker closed 8 months ago

AzureMarker commented 1 year ago

I think this is the first time anyone from homebrew has connected to the Circle Pad Pro (or they never talked about it online). It took a while, but I figured out you just have to be persistent during the connection phase (do immediate retries).

This PR adds a service wrapper for the ir:USER service, and a example/demo of using it to connect to the Circle Pad Pro (the main usage of the service as far as I can tell).

Note that the New 3DS internally emulates the Circle Pad Pro, so using ir:USER on a New 3DS system should give you the c-stick inputs. The ir:rst service is slightly different, where it only works on the New 3DS (doesn't give CPP inputs). Most retail 3DS games use ir:USER to be compatible with both old and new 3DS systems.

Reference links:

BTW if you're curious what I want to use this for: https://github.com/AzureMarker/n3ds-controller

Demo: CPP demo photo

Note: the screen no longer flickers after enabling double buffering.

https://user-images.githubusercontent.com/4417660/212457545-5f12e5b4-85f3-4faa-9552-74ce43bacce8.mp4

Meziu commented 1 year ago

I honestly thought circle pad pro was automatically recognised by the system (I don’t have one, since I own a New 3DS XL with a built-in C-Stick)

AzureMarker commented 1 year ago

I honestly thought circle pad pro was automatically recognised by the system (I don’t have one, since I own a New 3DS XL with a built-in C-Stick)

I thought the same thing, and initially started working on wrapping the ir:rst service. However, I found that the service only supports the New 3DS, and libctru already automatically initializes/uses it in the HID implementation: https://github.com/devkitPro/libctru/blob/e253c2c00543ab96067dc241de8dd32d72e6ec99/libctru/source/services/hid.c#L65-L67

They leave the hard work of getting the CPP input to the user :upside_down_face:

Meziu commented 1 year ago

They leave the hard work of getting the CPP input to the user 🙃

I guess that makes sense. They noticed the difficulty to try to find a CPP and decided to make Hid more “stable” and not include it (since Hid runs in all apps regardless during the initial setup). Also… not many people miss that accessory 😂

AzureMarker commented 1 year ago

Yeah I don't blame them, especially since it seems like no one knew how to get a connection working anyway. The Circle Pad Pro is pretty niche, but I really want that second joystick for my PC controller emulation :smiling_imp:.

SteveCookTU commented 1 year ago

Really neat!

With the idea of how IR is receiving packets back and forth, is it worth possibly creating almost like an "IRServer" or "IRThread", etc that internally manages waiting for packets? This way svc doesn't have to be exposed to the user similar to the cam service.

My thought is it would be more user safe if a mpsc receiver channel was returned from the initialization and you could use recv instead of a more raw call to svc.

The connect function could be moved to the IrUser module and pass in an &Hid, the device id (if there are more such as the pokewalker), and a button combination that could cancel the connection loop. At the different error points return a specific error and at the end return a mpsc::receiver or something. Thoughts?

I would write up an example but I'm currently on mobile.

AzureMarker commented 1 year ago

ctru-rs is supposed to be a thin or minimal wrapper around libctru which makes it nicer and safer to use than raw C calls. With that in mind, we don't want to introduce more overhead or complexity than necessary, so the user application (or another library) should be where that IR thread and other connection logic goes. Especially when it involves handling user input. We don't want to force developers to handle initialization of the service/CPP in a specific way like the demo (i.e. taking in Hid and blocking execution). The suggestions are interesting and would be fine in their own crate though.

I think it's fine to expose some svc calls, especially when it's almost harmless like the one used here.

SteveCookTU commented 1 year ago

In that case, if some of the SVC functions are added then the cam module could be simplified.

I guess the main concern would be the safety implications of the exposed functions. Ex. An invalid, null, or non-event handle being passed

SteveCookTU commented 1 year ago

My N3DS XL won't recognize the c-stick as a CPP, it seems. It tries to check the infrared instead (I used my iPhone X's FaceID to shoot infrared in the ir sensor and it returns an error! 😆).

Found this on the libctru docs. Note that the main service for New3DS HID is ir:rst, and these two service are mutually exclusive: when one is initialized and reading data from New3DS HID, the other cannot access it.

The example is initializing hid which in turn initializes ir:rst within libctru. My guess as to why it isn't getting inputs from the c-stick.

Meziu commented 1 year ago

Found this on the libctru docs. Note that the main service for New3DS HID is ir:rst, and these two service are mutually exclusive: when one is initialized and reading data from New3DS HID, the other cannot access it.

The example is initializing hid which in turn initializes ir:rst within libctru. My guess as to why it isn't getting inputs from the c-stick.

Ok, great, we just have to make a runtime check for New3DS and state the example won’t work without the actual CPP.

SteveCookTU commented 1 year ago

Something to note:

There are a handful of devices that utilize ir including the NFC Reader, PokeWalker, and even some games directly access it (old Pokemon games syncing for local play). I'm curious how ir is utilized to handle the different packets since everything past hardware probably doesn't have a hardcoded device ID.

AzureMarker commented 1 year ago

Found this on the libctru docs. Note that the main service for New3DS HID is ir:rst, and these two service are mutually exclusive: when one is initialized and reading data from New3DS HID, the other cannot access it.

The example is initializing hid which in turn initializes ir:rst within libctru. My guess as to why it isn't getting inputs from the c-stick.

Maybe if the ir:USER service gets initialized before HID it will work. I don't have a New 3DS to test with (closest is Citra), but I'll try pushing that change to the example.

Meziu commented 1 year ago

@AzureMarker I understand if you don't have time to work on this, but this PR has been stale for a while. Do you intend on working on this? I would take care of it myself if I could, but I don't own a CirclePadPro.

AzureMarker commented 1 year ago

Yeah I definitely plan on merging this! I've just been busy with work and life recently 🙃. Thanks for keeping up the momentum!

wheremyfoodat commented 1 year ago

Hi! I work on https://github.com/wheremyfoodat/Panda3DS and have been having some trouble with CPP emulation. Trying to set up Rust and this fork of ctru-rs has been giving me some headaches, so is there perhaps any simple CPP test ROM you have using this? Preferably something that does output via svcOutputDebugString too.

Thanks for your work! It's nice to see someone making CPP homebrew a thing. Also ctru-rs is very cool too.

Meziu commented 1 year ago

Hi! I work on https://github.com/wheremyfoodat/Panda3DS and have been having some trouble with CPP emulation.

I'm happy to see you here! I've had more than a look at your project so I'm glad to see our work has sparked your interest.

Trying to set up Rust and this fork of ctru-rs has been giving me some headaches, so is there perhaps any simple CPP test ROM you have using this? Preferably something that does output via svcOutputDebugString too.

Sadly this PR has stagnated for quite a while and isn't up-to-date with our current build systems and integration. I would have liked to finish working on this myself, however I'm not in possession of an Old3DS with a CirclePadPro, so I can't test and develop this module.

Luckily, the fixes needed to get the example working are easy for who's used to the Rust toolchain, so I quickly built it for you.

Here's a google drive folder with the .3dsx files. The provided examples do not use svcOutputDebugString, rather printing to the stdout.

Again, I'm incredibly sorry as this is the most I can do. If there are further issues, I can guide you in the process of building the examples yourself (which is already very well documented in our wiki and for which you'll need cargo-3ds).

wheremyfoodat commented 1 year ago

Thank you so so much ❤❤❤

AzureMarker commented 11 months ago

Sadly this PR has stagnated for quite a while and isn't up-to-date with our current build systems and integration. I would have liked to finish working on this myself, however I'm not in possession of an Old3DS with a CirclePadPro, so I can't test and develop this module.

I'm really looking forward to getting time to work on this and merge it, eventually! It definitely will be merged by end of this year. I'll have more time over winter vacation :). Unfortunately lots of life stuff is higher priority than having fun with side projects :/.

Meziu commented 9 months ago

Took the freedom of merging the master branch into this. At least this PR should result more usable and up-to-date than before, even though I still can't really work on it.

wheremyfoodat commented 9 months ago

Sadly this PR has stagnated for quite a while and isn't up-to-date with our current build systems and integration. I would have liked to finish working on this myself, however I'm not in possession of an Old3DS with a CirclePadPro, so I can't test and develop this module.

I'm really looking forward to getting time to work on this and merge it, eventually! It definitely will be merged by end of this year. I'll have more time over winter vacation :). Unfortunately lots of life stuff is higher priority than having fun with side projects :/.

:D

AzureMarker commented 8 months ago

Did some work on the PR, but it needs a little more still. The double buffering change seems to have actually increased the stuttering/flashing, so not sure what I'm doing wrong there. Edit: fixed!

I'll also need to open a PR on test-runner since it depends on Console, and I had to change the signature to make double buffering work.

Jhynjhiruu commented 8 months ago

Wow, this is very fortuitous timing - I'm putting together a project that uses citro3d, and I've done some of the work to get bindings for the texture code in nice Rust. The last thing it needs in order to be a good demonstration for using the bindings to render a proper model is C-Stick support for moving the camera around; I've made do with the gyroscope so far, but it's not ideal.

AzureMarker commented 8 months ago

@Jhynjhiruu If you have a New 3DS you can use the ir:rrst service which is initialized by the hid component in libctru automatically. It will give the New 3DS c-stick position (roughly, just the main direction): https://libctru.devkitpro.org/hid_8h.html#a2f80701c36e79c0640d91c788feee0b3a557de4e103c1e2c316b9e0962c221ab6

Jhynjhiruu commented 8 months ago

@Jhynjhiruu If you have a New 3DS you can use the ir:rrst service which is initialized by the hid component in libctru automatically. It will give the New 3DS c-stick position (roughly, just the main direction): https://libctru.devkitpro.org/hid_8h.html#a2f80701c36e79c0640d91c788feee0b3a557de4e103c1e2c316b9e0962c221ab6

I do have a New 3DS, but I also have an Old 3DS and a Circle Pad Pro, and I don't want to limit my code to just working on one of them. Plus, not having analogue controls for the camera wouldn't be so fun.

AzureMarker commented 8 months ago

Nice! If you find any issues with this code and your Circle Pad Pro or New 3DS just let me know.

Jhynjhiruu commented 8 months ago

Nice! If you find any issues with this code and your Circle Pad Pro or New 3DS just let me know.

Should I implement it using this branch and report back?

AzureMarker commented 8 months ago

Yeah that should work

Jhynjhiruu commented 8 months ago

Yeah that should work

Alright, I'll give it a go

Jhynjhiruu commented 8 months ago

Interesting; I appear to have written code that works completely fine on my real Circle Pad Pro but doesn't support my New 3DS. Odd.

Jhynjhiruu commented 8 months ago

I'm not entirely sure what's going wrong here. IrUser is initialised before Hid, and yet it still doesn't receive any data from the C-Stick. My code also seems to be running very slowly, so it almost seems like one of the delays is wrong.

SteveCookTU commented 8 months ago

I'm not entirely sure what's going wrong here. IrUser is initialised before Hid, and yet it still doesn't receive any data from the C-Stick. My code also seems to be running very slowly, so it almost seems like one of the delays is wrong.

Unless there is another source, 3dbrew mentions that only one can read from it when both are initialized but nothing about the order. ir:rst may override completely or whichever is initialized last takes full control. This may have to be tested unless already done. (Unless you aren't using ir:rst at all then this can be ignored!)

Jhynjhiruu commented 8 months ago

I'm not entirely sure what's going wrong here. IrUser is initialised before Hid, and yet it still doesn't receive any data from the C-Stick. My code also seems to be running very slowly, so it almost seems like one of the delays is wrong.

Unless there is another source, 3dbrew mentions that only one can read from it when both are initialized but nothing about the order. ir:rst may override completely or whichever is initialized last takes full control. This may have to be tested unless already done. (Unless you aren't using ir:rst at all then this can be ignored!)

I don't... think I'm using ir:rst? Either way, the example in this PR (ir-user-circle-pad-pro.rs) doesn't work on my New 3DS. The input response is the same as in my project:

CirclePadProInputResponse {
    c_stick_x: 0x800,
    c_stick_y: 0x800,
    battery_level: 0x1f,
    zl_pressed: false,
    zr_pressed: false,
    r_pressed: false,
    unknown_field: 0x0,
}

The ReceiveBuffer seems to be doing something, it's just not what it's supposed to be. Comparing to my real Circle Pad Pro, the ReceiveBuffer seems to update much more slowly, and the data seems to be f3 a5 20 06 10 00 08 80 ff 00 on repeat. I notice that the real CPP's IrUserStatusInfo.connection_role is 1, whereas on my New 3DS it's 2. Otherwise, all of the info appears to be identical or equivalent. I'm a bit stumped.

SteveCookTU commented 8 months ago

According to a comment in the example:

// Initialize HID after ir:USER because libctru also initializes ir:rst,
// which is mutually exclusive with ir:USER. Initializing HID before ir:USER
// on New 3DS causes ir:USER to not work.

I haven't tested the example yet on N3DS myself to see if there are any issues. Seems like complications will be particular to the N3DS though so playing around with initialization ordering or other small things may offer ideas since this doesn't seem to be too well documented.

Jhynjhiruu commented 8 months ago

According to a comment in the example:

// Initialize HID after ir:USER because libctru also initializes ir:rst,
// which is mutually exclusive with ir:USER. Initializing HID before ir:USER
// on New 3DS causes ir:USER to not work.

I haven't tested the example yet on N3DS myself to see if there are any issues. Seems like complications will be particular to the N3DS though so playing around with initialization ordering or other small things may offer ideas since this doesn't seem to be too well documented.

My code does initialise HID after ir:USER, I think, and the example definitely does. Neither works on my console.

AzureMarker commented 8 months ago

Have you tried not initializing HID via libctru at all?

Jhynjhiruu commented 8 months ago

Have you tried not initializing HID via libctru at all?

No, I haven't, since then I wouldn't have an easy way to exit the app. I'll try it though.

Jhynjhiruu commented 8 months ago

Have you tried not initializing HID via libctru at all?

No, I haven't, since then I wouldn't have an easy way to exit the app. I'll try it though.

Nope, didn't work. Still doesn't work.

Meziu commented 8 months ago

Have you tried not initializing HID via libctru at all?

Nope, didn't work. Still doesn't work.

Friendly reminder that libctru initialises a handle to hid by default here.

You may have to close Hid or do something else before being able to use the service properly.

Jhynjhiruu commented 8 months ago

Have you tried not initializing HID via libctru at all?

Nope, didn't work. Still doesn't work.

Friendly reminder that libctru initialises a handle to hid by default here.

You may have to close Hid or do something else before being able to use the service properly.

I had totally forgotten about that! However, disabling this (by defining a new #[no_mangle] unsafe extern "C" fn __appInit(), which calls srvInit() and nothing else) still doesn't fix the issue. Eh??

Meziu commented 8 months ago

Have you tried not initializing HID via libctru at all?

Nope, didn't work. Still doesn't work.

Friendly reminder that libctru initialises a handle to hid by default here. You may have to close Hid or do something else before being able to use the service properly.

I had totally forgotten about that! However, disabling this (by defining a new #[no_mangle] unsafe extern "C" fn __appInit(), which calls srvInit() and nothing else) still doesn't fix the issue. Eh??

Here, found it. You need to re-implement this weak function https://github.com/devkitPro/libctru/blob/d0936b879bd2088cae61b69707fe70cb66ede651/libctru/source/services/hid.c#L35. If you set the return to false any use of hid will not use iirst.

Jhynjhiruu commented 8 months ago

Have you tried not initializing HID via libctru at all?

Nope, didn't work. Still doesn't work.

Friendly reminder that libctru initialises a handle to hid by default here. You may have to close Hid or do something else before being able to use the service properly.

I had totally forgotten about that! However, disabling this (by defining a new #[no_mangle] unsafe extern "C" fn __appInit(), which calls srvInit() and nothing else) still doesn't fix the issue. Eh??

Here, found it. You need to re-implement this weak function https://github.com/devkitPro/libctru/blob/d0936b879bd2088cae61b69707fe70cb66ede651/libctru/source/services/hid.c#L35. If you set the return to false any use of hid will not use iirst.

That still doesn't fix the issue. At this point I'm convinced there's a bug of some kind. I think it might be worth adding a default __appInit() function somewhere in ctru-rs that does what I've just done, since we generally want to get our own handles to Hid etc. and there's not much point initialising more than we need.

wheremyfoodat commented 8 months ago

I obtained the following log from Panda3DS, running Majora's Mask 3D, logging only srv::GetServiceHandle and any calls to HID and ir:user functions:

...
srv::getServiceHandle (Service: hid:USER, nameLength: 8, flags: 0)
HID::GetIPCHandles
HID::EnableAccelerometer
HID::EnableGyroscopeLow
HID::GetGyroscopeLowCalibrateParam
HID::GetGyroscopeLowRawToDpsCoefficient
HID::GetGyroscopeLowCalibrateParam
HID::GetGyroscopeLowRawToDpsCoefficient
...

Later on, when loading a save and the game attempts to initialize CPP:

srv::getServiceHandle (Service: ir:USER, nameLength: 7, flags: 0)
IR:USER: InitializeIrnopShared (shared mem size = 00001000, sharedMemHandle = 2C) 
IR:USER: GetConnectionStatusEvent
IR:USER: RequireConnection (device: 1)

So initializing HID before ir:USER seems to be fine, but it does indeed seem that libctru initializing ir:rst during hidInit on New 3DS might be problematic.

Jhynjhiruu commented 8 months ago

I obtained the following log from Panda3DS, running Majora's Mask 3D, logging only srv::GetServiceHandle and any calls to HID and ir:user functions:

...
srv::getServiceHandle (Service: hid:USER, nameLength: 8, flags: 0)
HID::GetIPCHandles
HID::EnableAccelerometer
HID::EnableGyroscopeLow
HID::GetGyroscopeLowCalibrateParam
HID::GetGyroscopeLowRawToDpsCoefficient
HID::GetGyroscopeLowCalibrateParam
HID::GetGyroscopeLowRawToDpsCoefficient
...

Later on, when loading a save and the game attempts to initialize CPP:

srv::getServiceHandle (Service: ir:USER, nameLength: 7, flags: 0)
IR:USER: InitializeIrnopShared (shared mem size = 00001000, sharedMemHandle = 2C) 
IR:USER: GetConnectionStatusEvent
IR:USER: RequireConnection (device: 1)

So initializing HID before ir:USER seems to be fine, but it does indeed seem that libctru initializing ir:rst during hidInit on New 3DS might be problematic.

Don't suppose you can determine the parameters MM uses for initialising the CPP? Timings etc. Just overriding hidShouldUseIrrst() to return false doesn't change anything, irritatingly.

wheremyfoodat commented 8 months ago

I have a half-decompilation of the Majora's Mask IR code on my computer, though it's rather complicated. "Timing" is done via the IR events (eg once the game calls IR::RequireConnection to pair with the CPP, it uses WaitSync1 on the IR connection event. This way it sleeps until the connection attempts has ended and IR shared memory is properly initialized. After that, I think it uses the send/recv events for synchronizing package transfers)

Jhynjhiruu commented 8 months ago

I have a half-decompilation of the Majora's Mask IR code on my computer, though it's rather complicated. "Timing" is done via the IR events (eg once the game calls IR::RequireConnection to pair with the CPP, it uses WaitSync1 on the IR connection event. This way it sleeps until the connection attempts has ended and IR shared memory is properly initialized. After that, I think it uses the send/recv events for synchronizing package transfers)

@AzureMarker Any chance of trying something like this out?

AzureMarker commented 8 months ago

I'm pretty sure that's how it already works. I based my implementation on Majora's Mask (even got into quite a few gdb sessions).

Jhynjhiruu commented 8 months ago

I'm pretty sure that's how it already works. I based my implementation on Majora's Mask (even got into quite a few gdb sessions).

Welp. Is there anything else I can do? Info to capture?

AzureMarker commented 8 months ago

Is there any way to know if the ir:rst service is in use? Maybe something else is initializing it. Or maybe calling the close function could help kill whatever active session there might be? I don't have a New 3DS to test with.

Jhynjhiruu commented 8 months ago

Is there any way to know if the ir:rst service is in use? Maybe something else is initializing it. Or maybe calling the close function could help kill whatever active session there might be? I don't have a New 3DS to test with.

The handy variable that says whether ir:rst is in use is unfortunately static, so I can't read it. However, the handles and the pointer to shared memory aren't static, so I can read those just fine, and they're all 0s, which strongly implies that ir:rst isn't in use.

AzureMarker commented 8 months ago

@ian-h-chamberlain @Meziu I think all the comments have been addressed, so let me know if we can merge the PR or if you have more comments. Thanks for taking a look! It's almost been a year since I opened the PR :sweat_smile:.

Jhynjhiruu commented 8 months ago

@ian-h-chamberlain @Meziu I think all the comments have been addressed, so let me know if we can merge the PR or if you have more comments. Thanks for taking a look! It's almost been a year since I opened the PR 😅.

Well, it would be nice to get the example code working on New 3DS first...