Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 46 commits into from
Jan 2, 2024

Conversation

AzureMarker
Copy link
Member

@AzureMarker AzureMarker commented Jan 14, 2023

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.

CPP.demo.video.compressed.mp4

@Meziu
Copy link
Member

Meziu commented Jan 14, 2023

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
Copy link
Member Author

AzureMarker commented Jan 14, 2023

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 🙃

@Meziu
Copy link
Member

Meziu commented Jan 14, 2023

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
Copy link
Member Author

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 😈.

@SteveCookTU
Copy link
Contributor

SteveCookTU commented Jan 14, 2023

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
Copy link
Member Author

AzureMarker commented Jan 15, 2023

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
Copy link
Contributor

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

@AzureMarker AzureMarker marked this pull request as ready for review January 28, 2023 22:17
Copy link
Member

@Meziu Meziu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the New 3DS internally emulates the Circle Pad Pro, so using ir:USER on a New 3D 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.

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! 😆).

I can't really review the example/module if I can't run it 🤔

ctru-rs/src/services/srv.rs Outdated Show resolved Hide resolved
@SteveCookTU
Copy link
Contributor

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
Copy link
Member

Meziu commented Jan 29, 2023

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
Copy link
Contributor

SteveCookTU commented Jan 29, 2023

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.

@Jhynjhiruu
Copy link
Contributor

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
Copy link
Member

Meziu commented Dec 27, 2023

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
Copy link
Contributor

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
Copy link
Member

Meziu commented Dec 27, 2023

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
Copy link
Contributor

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
Copy link

wheremyfoodat commented Dec 27, 2023

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Contributor

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
Copy link
Member Author

@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 😅.

@Jhynjhiruu
Copy link
Contributor

@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...

@AzureMarker
Copy link
Member Author

I don't have a New 3DS to test with, and it works in Citra. We can merge this and work on improving New 3DS support as a follow-up. The PR has been open for a very long time and is close to merging.

@ian-h-chamberlain
Copy link
Member

@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 😅.

LGTM! As you said, N3DS issues could probably be fixed in a follow up just to get this merged since it has been so long.

Copy link
Member

@Meziu Meziu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice PR, this is very much ready for merging!

I've also done a bit of testing with ir:rst and ir:USER on the side, and I can say a couple of things to help the discussion.

First of all, the ir:rst service is NOT initialized when implementing the libctru weak function as such:

#[no_mangle]
unsafe extern "C" fn hidShouldUseIrrst() -> bool {
    false
}

I could test various uses of the irrst service and they actually stopped working after using this implementation.

Secondly... to me this function seems to make it work! Not for long though. I can clearly see the C-Stick readings to be correct for some fraction of a second (not much more than 6 packets in a row), right before dying out and never reading anything again. This behavior (which is quite consistent, it seems) clearly means that there is something else missing for the implementation to work correctly, though I'm pretty much unable to read any further into it.

This PR can be merged, but you should open an issue since more research needs to be made.

ctru-rs/examples/ir-user-circle-pad-pro.rs Show resolved Hide resolved
ctru-rs/examples/ir-user-circle-pad-pro.rs Show resolved Hide resolved
ctru-rs/examples/ir-user-circle-pad-pro.rs Show resolved Hide resolved
ctru-rs/src/services/gfx.rs Outdated Show resolved Hide resolved
ctru-rs/src/services/ir_user.rs Outdated Show resolved Hide resolved
ctru-rs/src/services/ir_user.rs Outdated Show resolved Hide resolved
ctru-rs/src/services/ir_user.rs Outdated Show resolved Hide resolved
@AzureMarker AzureMarker requested a review from Meziu January 2, 2024 22:37
@AzureMarker AzureMarker merged commit 157824a into master Jan 2, 2024
4 checks passed
@AzureMarker AzureMarker deleted the feature/ir-user branch January 2, 2024 22:54
@AzureMarker
Copy link
Member Author

Opened #151

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants