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

USB: Add train controller emulation #11723

Merged
merged 5 commits into from
Aug 31, 2024
Merged

Conversation

joestringer
Copy link
Contributor

@joestringer joestringer commented Aug 18, 2024

Description of Changes

Emulate a range of "Densha Mascon" train controllers originally sold by Taito, such as TCP20009, TCP20011, TCP20014. Built using datasheets courtesy of @marcriera and other contributors at https://marcriera.github.io/ddgo-controller-docs/ .

Type 2 Shinkansen Ryojōhen
type2 shinkansen ryojouhen

Rationale behind Changes

Lever-based controllers provide an advantage over the standard DualShock emulation for some games, as these controllers physically hold an axis in a particular level without requiring the player to actively apply pressure. These changes allow players with lever-based controllers to play Densha de Go! titles such as:

  • Densha de GO! 3 Tsūkin-hen
  • Densha de GO! Final
  • Densha de GO! Professional 2
  • Densha de GO! Ryojōhen
  • Densha de GO! San'yō Shinkansen-hen

Suggested Testing Steps

The ddgo-controller-docs website describes these controllers and lists the compatible titles. I have tested these with my own USB Zuiki One-Handle MasCon controller for Nintendo Switch. Given the way that the hardware maps into the emulated controller, the best subtype for this particular hardware controller is the "Type 2" controller. Some games will change the input scheme and number of power/brake notch settings based on the selected subtype variant.

Related: #4763

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for submitting a contribution to PCSX2

As this is your first pull request, please be aware of the contributing guidelines.

Additionally, as per recent changes in GitHub Actions, your pull request will need to be approved by a maintainer before GitHub Actions can run against it. You can find more information about this change here.

Please be patient until this happens. In the meantime if you'd like to confirm the builds are passing, you have the option of opening a PR on your own fork, just make sure your fork's master branch is up to date!

@joestringer
Copy link
Contributor Author

cc @sonik-br this might be interesting to you. I have not tested the "USB passthrough" mode but I believe that this should allow original Densha De GO! USB controllers to work correctly with this patch.

@joestringer
Copy link
Contributor Author

Fixed a couple of CI complaints - extraneous xml tags in the vcxproj file, and I forgot to add the new files to the CMakeLists.txt, leading to linker failures in the CI builds. These should be resolved now.

Copy link
Contributor

@IlDucci IlDucci left a comment

Choose a reason for hiding this comment

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

Here's some suggestions to make life a bit easier for translators.

@sonik-br
Copy link

cc @sonik-br this might be interesting to you. I have not tested the "USB passthrough" mode but I believe that this should allow original Densha De GO! USB controllers to work correctly with this patch.

Amazing work!
I can try and report back. There's a build to download or I need to build from source?

@kamfretoz
Copy link
Contributor

There's a build to download or I need to build from source?

You can download the CI artifacts from the checks tab, make sure to pick your OS.

image

@sonik-br
Copy link

Not sure how to use usb passthrough. The device is not HID and does not shows as a gamepad under windows.
I need to install generic a usb driver with zadig?

@Florin9doi
Copy link
Contributor

Not sure how to use usb passthrough. The device is not HID and does not shows as a gamepad under windows. I need to install generic a usb driver with zadig?

Won't work. Only HID controllers like DGOC-44U will work

@sonik-br
Copy link

Won't work. Only HID controllers like DGOC-44U will work

Ah ok!
I can emulate this device using a rp2040 then. Can try it later this week.

@joestringer
Copy link
Contributor Author

@Florin9doi @IlDucci thanks for the reviews! I believe I've addressed all of your comments.

Copy link
Contributor

@IlDucci IlDucci left a comment

Choose a reason for hiding this comment

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

Thanks for hearing translators out! Unfortunately, I missed one element that could probably use a hint for translators (who might not expect train stuff happening in an emulator).

@sonik-br
Copy link

I could no get passthrough to work.
I'm emulating a PC Two Handle (DGOC-44U) using the descriptors from marcriera.
Need any special setting?

@Florin9doi
Copy link
Contributor

What kind of passthrough do you expect? You'll still have to map the axes and buttons , but the axes values won't stick to predefined values and will be sent unchanged.

@sonik-br
Copy link

What kind of passthrough do you expect? You'll still have to map the axes and buttons , but the axes values won't stick to predefined values and will be sent unchanged.

Brake axis can't be mapped. It does not pick any movement on the map screen.
But the axis is working as I can test on windows and on the PC version of the game (DDGO Final)

@Florin9doi
Copy link
Contributor

The brake's range is too limited and is ignored? Try to map a random button then open PCSX2.ini and change Brake=Xinput-X/ButtonY to Brake=Dinput-x/FullAxis1

@sonik-br
Copy link

sonik-br commented Aug 20, 2024

Still not working.
Tested with fullaxis0 and fullaxis1.

[USB1]
Type = DenshaCon
DenshaCon_A = DInput-0/Button1
DenshaCon_B = DInput-0/Button0
DenshaCon_C = DInput-0/Button2
DenshaCon_D = DInput-0/Button3
DenshaCon_Select = DInput-0/Button4
DenshaCon_Start = DInput-0/Button5
DenshaCon_Up = DInput-0/Button6
DenshaCon_Right = DInput-0/Button9
DenshaCon_Down = DInput-0/Button7
DenshaCon_Left = DInput-0/Button8
DenshaCon_Power = DInput-0/+Axis1~
DenshaCon_Brake = DInput-0/FullAxis0
DenshaCon_Passthrough = true

Also tested with

DenshaCon_Power = DInput-0/FullAxis1
DenshaCon_Brake = DInput-0/FullAxis0

@joestringer
Copy link
Contributor Author

joestringer commented Aug 21, 2024

@Florin9doi @IlDucci I've resolved your latest feedback 🚀

@sonik-br thanks for testing. Do the other buttons and power lever work in-game or are all of the controls broken? If they work, does the lever increase the number of power notches that you expect?

I also wonder if the brake axis is a much higher number. For my controller, despite there being only a single lever with two axes (power, brake), SDL represents these with positive/negative axis 7.

@sonik-br
Copy link

@joestringer buttons and dpad works in-game.
Power lever does not. The game think it's always on max power.
image

@joestringer
Copy link
Contributor Author

joestringer commented Aug 22, 2024

@sonik-br Hmm OK. Early on I did encounter similar issues while initially developing the support. I can't do much about the brake since that relies on the input library to pull the values into the point where this feature processes it. Unless you're able to resolve that problem, the idea of the passthrough mode feature/patch might not make sense.

That said to debug further about what's going on with the power axis, you could try pulling this new version with debug logging here:

https://github.com/joestringer/pcsx2/actions

You might need to remap the controls again, I'm not sure. But after that you can go to Tools -> Enable Log Window or Enable File Logging. When you run the game, the log will spam lines about the brake and power values. If you can start with the controller with brakes and power off, then move the power lever step by step from released up to full power and back down, it will log all the input values. If you scan up through the logs until you see logs printing different values for the power axis and share those logs, it would help to understand the range of values that are being pulled into this code. Separately if you can figure out the brake, then it would also help to have the same log data lines with the varying brake input values.

@sonik-br
Copy link

@joestringer tested...
Forcing it on the ini file to fullaxis as:

TrainController_Power = DInput-0/FullAxis1
TrainController_Brake = DInput-0/FullAxis0

Brake only shows as FF on it's first position and FF on all other positions.
Power shows as:

P5 80
P4 A0
P3 Be
P2 D3
P1 ED
N FF
Transiton FF

All tests doe using direct input.

And interesting: Same results with and without passthrough mode

@joestringer
Copy link
Contributor Author

joestringer commented Aug 23, 2024

And interesting: Same results with and without passthrough mode

Oh yeah maybe I didn't explain well. The logs you see are presenting the raw input from DInput, before this code processes the values to output to the game. So the passthrough mode doesn't have an effect on the values you see there. That's normal.

I can't really explain the discrepancy.. If you're running a DGOC-44U, then these docs describe the notches as:

N P1 P2 P3 P4 P5 Transition
0x81 0x6D 0x54 0x3F 0x21 0x00 0xFF

Comparing this with the values that you have presented, there values are very different:

N P1 P2 P3 P4 P5 Transition
0xFF 0xED 0xD3 0xBe 0xA0 0x80 0xFF

The passthrough mode was written with the idea in mind that the values from the input library would be identical to the selected subtype of controller. But even if I look through the other controller types in the Marc Riera datasheets, I can't find any controller types that match the values you are reporting.

One interesting thing about the bitpatterns you report is that the bit at offset 0x80 is always set. On average across the values they're all around 0x7E-0x80 higher than the value reported by the docs. This makes me wonder whether either model you have works differently somehow, or if perhaps the values get treated as signed values that get set negative somewhere, or if somehow the input library is doing some conversions that cause a skewing of the values into the upper range.. Not sure if FullAxis vs. +Axis vs -Axis configuration could have this sort of impact as well.

Technically it's not that difficult to try to adjust for this, but it's a bit hard for me to picture the range of devices that may be impacted by this behaviour - is this a general property of all of the original controllers so I should apply a filter automatically in passthrough mode? Or is it that the axis needs to be configured in a particular way to cause these results? Will a fix I come up with help or hurt the emulation of this controller? I'd welcome input from upstream PCSX2 devs if they have ideas on how to best handle this. I guess I could add another separate setting option called "DGOC-44U compatibility" if we think that it's this specific controller that has this behaviour. On the other hand, another option is just to remove passthrough mode if it's not going to fully work for someone.

In the mean time, just to see if it could work, I added a hack to the latest builds over here to try to have the right behaviour for your environment: https://github.com/joestringer/pcsx2/actions . The code is in joestringer#2.

Brake only shows as FF on it's first position and FF on all other positions.

OK. That confirms that Dinput isn't processing the brake signal at all. Pretty much what we expected since you can't even configure it through the UI.

@joestringer
Copy link
Contributor Author

joestringer commented Aug 24, 2024

I may have underestimated the complexity of connecting this logic with real-world DDG controllers ;) Sorry about that. This PR was initially designed just so I could get my Zuiki controller to work. The Zuiki connects like a regular joystick controller into Windows, then outputs various axis values through the positive and negative Y axes when I connect it using SDL. With the passthrough mode turned off, that maps accurately into the game and I can play without any issues. I assume that any other joystick which outputs values in a similar manner should work the same, potentially including things like HOTAS controllers (though I also don't have one of those to try out).

What I didn't realize is that some of these older joysticks do not implement HID, and that there may be issues with some of the input libraries in getting the signal into the code that this PR touches, whether SDL, Xinput or Dinput. When I sketched up the patch for axis passthrough, I knew that the baseline functionality made different assumptions about the joystick's output value range compared with the spec sheets, so in an ideal world I would take whatever signal that those controllers provided on the relevant axis and send it straight to the game. My naive hope was that this would "just work", but I don't have the physical hardware to test & debug it. As y'all have provided feedback, it's becoming more and more obvious that there are subtleties to the problem space that I just don't know about. I don't necessarily know enough at the moment to be able to solve all the problems for everyone.

For @sonik-br's report, I could potentially get it working by tweaking the options to allow (a) inverting the axis and (b) scaling the full 256-value axis down into a 128-bit output value to the game. I can throw that together. That said, if the brake is still not pushing a full range of values into the controller emulation layer through SDL then I can't solve that. In the end you're gonna want to use both power and brake to actually play.

Similarly, for @mario032106, if you can get both axes configured in the settings window for power/brake (and all the buttons), and if that then outputs different values into the console for all the different power/brake notches, then there's a chance I could introduce tweaks into these patches to match the input to support your joystick. There may be a bit of tedious back/forth to get it working, but it should be doable as long as the pcsx2 controller emulation layer can get the full range of axis signal inputs.

Regardless of all that, even without the USB passthrough support, I think this PR could be useful at least for players with a Zuiki, if not some other lever-like joystick controllers, so worst case I could just drop the USB passthrough patch and defer that to a subsequent PR (and maybe try to work more closely with some of you to get this emulation working with a wider range of hardware controllers).

@joestringer
Copy link
Contributor Author

Oh, and naming is one of the hardest problems in computer science ;) Maybe it would help if I came up with a better name than "USB passthrough" as I agree with @mario032106 that the name is a bit misleading since it can't get raw access to the original controllers.

@joestringer
Copy link
Contributor Author

I used Zadig to install the WinUSB driver for it so I could test it with AutoTrainTAS (couldn't get it to recognize it otherwise), could it be that that's interfering with it?

As far as I understand, the denconv tool by AutoTrainTAS takes input from Windows input and then monitors for the DDG programs running and edits the memory of the running application to inject the controls into the game. Assuming that's correct, I wouldn't expect that denconv interferes with this. I could always be wrong so let me know if you observe otherwise.

@sonik-br
Copy link

sonik-br commented Aug 24, 2024

@joestringer thanks for taking your time and doing this!

In my case, I'm using a (open source and simple to build) custom usb adapter to read a PS2 device (non-hid) and output as a PC One handle or Two handle device.
I can also make it output like the ddgo Nintendo Switch controller, or even create a custom output, to match what the emulator (and your code) expects.

Edit: I've adjusted the adapter to output as a Nintendo Switch controller and it's working now on the emulator (without passthrough)

joestringer and others added 5 commits August 24, 2024 10:22
Add support for TCPP20009 controllers, datasheets courtesy of Marc Riera. Tested with a One Handle MasCon for Nintendo Switch as the controller device.

Link: https://marcriera.github.io/ddgo-controller-docs/controllers/usb/tcpp20009/
Add support for TCPP20011 controllers, datasheets courtesy of Marc Riera. Tested with a One Handle MasCon for Nintendo Switch as the controller device.

Link: https://marcriera.github.io/ddgo-controller-docs/controllers/usb/tcpp20011
Add support for TCPP20014 controllers, datasheets courtesy of Marc Riera. Tested with a One Handle MasCon for Nintendo Switch as the controller device.

Link: https://marcriera.github.io/ddgo-controller-docs/controllers/usb/tcpp20014/
Enable an option to use native Densha De Go! controllers and pass the axis inputs from the controller directly through to the game.
@joestringer
Copy link
Contributor Author

@Florin9doi ah, the extra pair of eyes is much welcomed. I fixed that up and confirmed with the latest version that the unprocessed input values are passed through to the game.

@sonik-br very cool! Glad to hear the Nintendo DDG mode is working for you. Hopefully now the axis passthrough mode should also work for DGOC-44U. It still won't resolve any issues if the input library can't detect the axis, but it should fix issues where PCSX2 reads the axis value and interprets it in a way that causes the throttle or brake to be "stuck" at some level and won't change when you move the axis.

I've updated the branch here with the latest fixes and without the verbose logging to the console. I've also pushed a separate build here with additional logging in case anyone wants to try to figure out what's happening with the raw inputs. Builds available here: https://github.com/joestringer/pcsx2/actions .

@sonik-br
Copy link

Still a no go for me with DGOC-44U.
Same range as before (0x80 to 0xff)

Power 5 shows as:
DenshaCon data: brake={in:00,out:00} power={in:80,out:80} type=0 passthrough=1

@joestringer
Copy link
Contributor Author

@sonik-br OK, I've just pushed a new experimental approach to this over at joestringer#2 / https://github.com/joestringer/pcsx2/actions. With the new approach there is a specific setting for DGOC-44U which will adjust the power values based on the input you've provided earlier in this thread. You could give that a go and see if it helps.

@sonik-br
Copy link

Tested.
I believe that the out values are off...

Type = TrainController
TrainController_Compatibility = DGOC-44U
TrainController_Power = SDL-0/FullAxis7
N DenshaCon data: brake={in:00,out:79} power={in:ff,out:7f} type=0 compat=2
1 DenshaCon data: brake={in:00,out:79} power={in:ed,out:6d} type=0 compat=2
2 DenshaCon data: brake={in:00,out:79} power={in:d3,out:53} type=0 compat=2
3 DenshaCon data: brake={in:00,out:79} power={in:be,out:3e} type=0 compat=2
4 DenshaCon data: brake={in:00,out:79} power={in:a0,out:20} type=0 compat=2
5 DenshaCon data: brake={in:00,out:79} power={in:80,out:00} type=0 compat=2

@joestringer
Copy link
Contributor Author

joestringer commented Aug 24, 2024

👍 I see. Does the game recognize the notches or not? It's weird to me that notch 1 and 5 are correct but the others are off by a little. I can hack this to make it work accurate for this case, but I don't know why this is happening and whether it would help for others running the same controller. Either way I uploaded a new PR in my repo that should capture the right values hopefully. Not sure how the behaviour will be when the lever is in transition though, since the Neutral value is the same as transition in your reports.

@sonik-br
Copy link

This fixed the power handle!
But brake still shows as input 0xf9 and 0xff only.
I believe that this might be related to windows only. Windows does not pass raw HID data.
Not sure if there's anything that can be done to fix this.

@Florin9doi
Copy link
Contributor

Florin9doi commented Aug 26, 2024

I made a dgoc44u clone and I found that both windows and linux are reading the axes as signed int8 although the controller declares positive values and so half of values are reported as negative. And even more, Windows applies some built-in calibration and the values get shifted by one near the center.

This is a draft for dgoc44u, but it should be probably applied behind a new setting; signed/unsigned mode. 0.4995f is used to compensate for calibration/rounding error, but would be better to make the axes stick to the nearest known value.

               switch (bind_index)
                {
                        case CID_TC_POWER:
-                               s->data.power = static_cast<u32>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
+                               if (value < 0.5f)
+                                       s->data.power = 0x81 + static_cast<u32>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
+                               else
+                                       s->data.power = static_cast<u32>(std::clamp<long>(std::lroundf((value - 0.4995f) * 255.0f), 0, 255));
                                break;
                        case CID_TC_BRAKE:
-                               s->data.brake = static_cast<u32>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
+                               if (value < 0.5f)
+                                       s->data.brake = 0x81 + static_cast<u32>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
+                               else
+                                       s->data.brake = static_cast<u32>(std::clamp<long>(std::lroundf((value - 0.5f) * 255.0f), 0, 255));
                                break;

@lightningterror
Copy link
Contributor

Is this good to go for merge?

@Florin9doi
Copy link
Contributor

Is this good to go for merge?

Yes, one of the features doesn't work as intended, but it can be improved later.

@lightningterror lightningterror merged commit ab2d18e into PCSX2:master Aug 31, 2024
11 checks passed
@joestringer joestringer deleted the mascon branch September 3, 2024 00:08
@joestringer
Copy link
Contributor Author

Ah right, good spotting @Florin9doi.. I wasn't taking into account that the raw values are floats and by the time my debug logging printed the values, the values were already excluding values outside the input float values of 0.0-1.0.

I pushed a fresh build with this latest heuristic and a new compatibility mode setting here: joestringer#2 . If anyone wants to try that out, make sure to set the DGOC-44U compatibility mode in the controller settings.

@sonik-br
Copy link

sonik-br commented Sep 3, 2024

Still not working for me :/

Brake only shows as:
brake_in=1.000000 brake_out=128 type=0 compat=1
brake_in=0.976195 brake_out=122 type=0 compat=1

And Power fron none to 5 (including transition state that shows as 1.0) :

power_in=1.000000 power_out=128 type=0 compat=1
power_in=0.928571 power_out=109 type=0 compat=1
power_in=1.000000 power_out=128 type=0 compat=1
power_in=0.829370 power_out=84 type=0 compat=1
power_in=1.000000 power_out=128 type=0 compat=1
power_in=0.746040 power_out=63 type=0 compat=1
power_in=1.000000 power_out=128 type=0 compat=1
power_in=0.627002 power_out=33 type=0 compat=1
power_in=1.000000 power_out=128 type=0 compat=1
power_in=0.500000 power_out=0 type=0 compat=1

@joestringer
Copy link
Contributor Author

OK, the power_in values seem to be a bit different from what @Florin9doi was reporting with their simulated controller. Again though even accounting for the raw input from the library I can't explain the limitations in the brake inputs.. that would have to be fixed separately to get this emulation feature working with that controller.

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

Successfully merging this pull request may close these issues.

7 participants