AndersMalmgren / FreePIE

Programmable Input Emulator
636 stars 145 forks source link

Add Force Feedback support to vjoy and joystick plugin #48

Open AndersMalmgren opened 9 years ago

AndersMalmgren commented 9 years ago

I have not checked if the C# SDK has the support yet, but here is a demo written in C http://sourceforge.net/p/vjoystick/code/HEAD/tree/branches/Incompatible/ForceFB/apps/FfbMon/FfbMon.cpp

http://vjoystick.sourceforge.net/site/index.php/forum/5-Discussion/393-force-feedback-support?start=20

cyberluke commented 1 year ago

It is completely messed up. I have forked it and I'm rewriting and fixing it here: https://github.com/cyberluke/FreePIE/tree/Ffb ...I was able to get Constant Force Effect to work (but direction is mapped wrong)

Issues: 1) Device.cs / CreateEffect method is creating effectParams[blockIndex] = new EffectParameters() - this is wrong because method SetConstantForce gets called before with magnitude parameter and this way you loose that parameter and effect has zero force, so it looks like it is not doing anything!

2) Documentation for vJoy says that there is no need to parse FFB packet on your own and that they provide a lot of FFB helper methods. I wasted 4 days trying to understand your code and even mailed Eric, but he just ignored it. All FFB Packet classes are wrong and EffectReportPacket is wrong. That's why you could not get Polar position, right? The correct implementation was in front of you whole time in Wrapper.cs (vJoy) and it is on line 215: public struct FFB_EFF_REPORT

        [StructLayout(LayoutKind.Explicit)]
        public struct FFB_EFF_REPORT
        {
            [FieldOffset(0)]
            public Byte EffectBlockIndex;
            [FieldOffset(4)]
            public FFBEType EffectType;
            [FieldOffset(8)]
            public UInt16 Duration;// Value in milliseconds. 0xFFFF means infinite
            [FieldOffset(10)]
            public UInt16 TrigerRpt;
            [FieldOffset(12)]
            public UInt16 SamplePrd;
            [FieldOffset(14)]
            public UInt16 StartDelay;
            [FieldOffset(16)]
            public Byte Gain;
            [FieldOffset(17)]
            public Byte TrigerBtn;
            [FieldOffset(18)]
            public Byte AxesEnabledDirection;
            [FieldOffset(20)]
            public bool Polar; // How to interpret force direction Polar (0-360°) or Cartesian (X,Y)
            [FieldOffset(24)]
            public UInt16 Direction; // Polar direction: (0x00-0x7FFF correspond to 0-360°)
            [FieldOffset(24)]
            public UInt16 DirX; // X direction: Positive values are To the right of the center (X); Negative are Two's complement
            [FieldOffset(26)]
            public UInt16 DirY; // Y direction: Positive values are below the center (Y); Negative are Two's complement
        }

3) I have updated vJoy library to the latest 2.2.1.0 version (nzj3 fork) - it contains some of FFB fixes as well and should support more than one effect now. Currently in FreePIE creating effect manually works. The only issue is with packet mapping and processing.

4) There might be some vendor specific FFB effect types. I'm testing this on Logitech G940 flight stick (I have also Microsoft SideWinder FFB 2 and Thrustmaster Alcantara driving wheel). Interesting is that Logitech SDK provides support for all these devices (they have C# and C++). Their force feedback library is same for both steering wheel and flight stick.

5) I did not commit anything to my fork yet, still needs one or two weeks of work. I plan to compile and backport this back to Windows XP for retrogaming. A lot of simulators do support force feedback, but their Steam repacked versions are very buggy and unfinished. On Windows XP everything works out of the box including proper resolution and EAX effects (yes, I use 4:3 monitor - CRT even now on Windows 10!)

Ideally this should be made compatible with Windows 98 as well, either backporting to some lower .NET version or using KernelEx and run it in Win XP compatibility (or compile it to native C++, DirectX 9.0c is there already and that is the only requirement).

MarijnS95 commented 1 year ago

@cyberluke you don't get anywhere by blaming someone for the the work they did 5 years ago. Feel free to work on this, I've long lost interest in a dead project.

cyberluke commented 1 year ago

Sorry, but I spent everyday till morning hours and asked only for a little help. The amount of time that has been invested from all the sides - would be nice to have some wrap up or knowledge sharing - that would be the best for the community.

MarijnS95 commented 1 year ago

@cyberluke I see no questions, only listing problems. If you want to be of any help, push your changes on top of existing work and make a PR out of it (linked branch contains no changes on top of Eric's fork).

If you're asking me to retroactively fix up my branch with aforementioned changes, state so explicitly.

cyberluke commented 1 year ago

Well, you see the question 4 days ago: "Hi, what is current status please? Is this in main branch or fork only?" ...then I contacted Eric directly and he replied via email. So it is mentioned in my investigation.

No, I'm not asking you to fix anything. I was only curious how the FFB packet was constructed and why. Like what was the source of information, because there is a lot of more stuff to be parsed and I wanted to continue on it. But I found the whole structure of it is not correct just now.

I have forked Eric's fork and applied 1 commit you added in 2022 (call to method Download) just to see if it would be of any help.

You don't know what I had to do. I had to literally look up Wine sources, decompile Direct Input DLL to get some information if I could pass the FFB packet directly to SlimDX (or SharpDX for that matter).

So I asked only for current status, where is the latest code and it would be nice to get some sources of documentation - like why you made FFB EffectReport packet the way it is. That would be all. That's what I call knowledge sharing.

cyberluke commented 1 year ago

So current CreateEffect hotfix would look like:

            TypeSpecificParameters parametersDefault = effectParams[blockIndex].Parameters;
            effectParams[blockIndex] = new EffectParameters()
            {
                Duration = duration,
                Flags = EffectFlags.ObjectIds | (polar ? EffectFlags.Polar : EffectFlags.Cartesian),
                Gain = gain,
                SamplePeriod = samplePeriod,
                StartDelay = startDelay,
                TriggerButton = triggerButton,
                TriggerRepeatInterval = triggerRepeatInterval,
                Envelope = null
            };
            effectParams[blockIndex].Parameters = parametersDefault;
cyberluke commented 1 year ago

and SetConstantForce hotfix would be:

        public void SetConstantForce(int blockIndex, int magnitude)
        {
            CheckFfbSupport("Unable to set constant force");

            if (effectParams[blockIndex].Parameters == null)
            {
                effectParams[blockIndex].Parameters = GetTypeSpecificParameter(EffectType.ConstantForce);
            }
            effectParams[blockIndex].Parameters.AsConstantForce().Magnitude = magnitude;
MarijnS95 commented 1 year ago

I would've looked for the latest branch for you after returning from holiday (there should be newer stuff that isn't on Eric's fork) but your comment is borderline disrespectful that it wasn't worth waiting for.

If you want to commence any knowledge sharing, look after your wording first. For example, you don't have the slightest clue what I went though when putting this together, without owning an FFB device of my own. Discuss the code instead of calling people out.

cyberluke commented 1 year ago

Well, I just haven't got time to use FFB device. I'm stuck in FEdit.exe (Force Editor Version 1.0 - 1999) - that is enough to fix the packets. Haha, yes, sorry, but I did not sleep well. I go to sleep everyday at 4AM because of this and then this was reaction after daily work and silence here (and my wife got pregnant a week ago and I try to put my maximum time here before I have even less time)

@MarijnS95 but it helped, see - one comment and see how you came to life nicely :-D

AndersMalmgren commented 1 year ago

@cyberluke you don't get anywhere by blaming someone for the the work they did 5 years ago. Feel free to work on this, I've long lost interest in a dead project.

I just wanted to comment on the dead project comment, according to google analytics we still have about 3k downloads a week, so the interest is there, there is just no one that have the time or interest to take over the project

cyberluke commented 1 year ago

And I'm CyberLuke. I'm not good with human interaction. Ok, thank you. Some sources of past information (like where the FFB packet structure came from would be nice). Otherwise, if you need anything, just ask me. Hopefully tonight I make some significant progress thanks to vJoy FFB helper classes and its interface.

But the added latency of FreePIE, reconstructing FFB packets and reprocessing them might be a bottleneck in the future.

Currently this FreePIE script works:

if starting:
    G940_JoystickID=3;
    G940_PedalsID=2;
    G940_ThrottleID=0;

    vJoy[0].registerFfbDevice(joystick[G940_JoystickID])
    #joystick[G940_JoystickID].createEffect(1, EffectType.ConstantForce, -1, [0, 1])
    #joystick[G940_JoystickID].setConstantForce(1, 5000)
    #joystick[G940_JoystickID].operateEffect(1, EffectOperation.Start, 0)
    diagnostics.debug("starting")

I will need to look up how to get rid of this:

    G940_JoystickID=3;
    G940_PedalsID=2;
    G940_ThrottleID=0;

and replace it by device name look up or GUID look up.

AndersMalmgren commented 1 year ago

And I'm CyberLuke. I'm not good with human interaction. Ok, thank you. Some sources of past information (like where the FFB packet structure came from would be nice). Otherwise, if you need anything, just ask me. Hopefully tonight I make some significant progress thanks to vJoy FFB helper classes and its interface.

But the added latency of FreePIE, reconstructing FFB packets and reprocessing them might be a bottleneck in the future.

Currently this FreePIE script works:

if starting:
    G940_JoystickID=3;
    G940_PedalsID=2;
    G940_ThrottleID=0;

    vJoy[0].registerFfbDevice(joystick[G940_JoystickID])
    #joystick[G940_JoystickID].createEffect(1, EffectType.ConstantForce, -1, [0, 1])
    #joystick[G940_JoystickID].setConstantForce(1, 5000)
    #joystick[G940_JoystickID].operateEffect(1, EffectOperation.Start, 0)
    diagnostics.debug("starting")

I will need to look up how to get rid of this:

    G940_JoystickID=3;
    G940_PedalsID=2;
    G940_ThrottleID=0;

and replace it by device name look up or GUID look up.

The joysticks should be accessable using their name too toy can both index on their index or their name.

cyberluke commented 1 year ago

@MarijnS95 The architecture with PacketMapper is nice. It uses generics and async processing.

Here are some notes for myself and what will be my next step: 1) FFBPacket.cs have two unmarshalling steps per packet. It always creates basePacket: basePacketData = GetPacketData<BasePacket>(); - that might be not needed. Now I'm not familiar with performance of Marshal.PtrToStructure(). It should be more efficient that creating new class instance, but there will be probably some overhead as well.

VJoy C# interface provides a C++ call for that (I plan to use that): Ffb_h_EffectBlockIndex

I will need to research the behavior and life cycle of struct in .NET. Then compare this approach to OOP. Old text editors (early Windows and perhaps even DOS) that use text styles and are written in OOP use Flyweight design pattern. That might be a nice inspiration. If we take into account that there is like 8 effect slots and the common use case is to have only one destination FFB device active, there could be a static pool of shared instances for Effects (& packets). Therefore on incoming packets, we won't create any new effect, just replace parameters. If we run out of pool objects, we would simply scale it up and create a new one on demand.

But it seems struct was the right decision, it would be good to limit the unmarshalling calls only, source here: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct

2) This method will need rewriting:

        public T GetPacketData<T>()
        where T : IFfbPacketData
        {
            return (T)Marshal.PtrToStructure(packet.PtrToData, typeof(T));
        }

Because it cannot be mapped like that. This created invalid values, which was noticeable when polar directions are being used.

Below is VJoy method Ffb_h_Eff_Report. You can see that the position of some bytes is reversed, so it is not possible to map it easily out of the box using [StructLayout(LayoutKind.Explicit)] and [FieldOffset(0)].

        Effect->EffectBlockIndex = Packet->data[1];
        Effect->EffectType = (FFBEType)(Packet->data[2]);
        Effect->Duration = (WORD)((Packet->data[4] << 8) + (Packet->data[3]));
        Effect->TrigerRpt = (WORD)((Packet->data[6] << 8) + (Packet->data[5]));
        Effect->SamplePrd = (WORD)((Packet->data[8] << 8) + (Packet->data[7]));
        Effect->StartDelay = (WORD)((Packet->data[10] << 8) + (Packet->data[9]));
        Effect->Gain = Packet->data[11];
        Effect->TrigerBtn = Packet->data[12];
        Effect->AxesEnabledDirection = Packet->data[13];
        Effect->Polar = (Packet->data[13] == 0x04);
        if (Effect->Polar)
            Effect->Direction = (WORD)((Packet->data[15] << 8) + (Packet->data[14]));
        else {
            if (Packet->size < (8+2+18)) return ERROR_INVALID_DATA; // the extra (2) is no longer optional

            Effect->DirX = (WORD)((Packet->data[15] << 8) + (Packet->data[14]));
            Effect->DirY = (WORD)((Packet->data[17] << 8) + (Packet->data[16]));
        }

So method public T GetPacketData() will need to provide some integration. For the sake of saving time, I will hack it using some procedural Switch block and return correct struct based on packet type.

3) Design enhancement: struct will be nested and will contain both original VJoy struct and custom BasePacket struct, for example FFB_EFF_REPORT in case of EffectReportPacket. This way it will stay compatible with VJoy, but also offer some additional functionality like IdxAndPacketType, DeviceId and BlockIndex.

BTW see this comment:

        // Some types don't carry Effect Block Index
        FFBPType Type;
        if (Ffb_h_Type(packet, &Type) != ERROR_SUCCESS)
            return ERROR_INVALID_DATA;
        if (Type == PT_CTRLREP ||
            Type == PT_SMPLREP ||
            Type == PT_GAINREP ||
            Type == PT_POOLREP ||
            Type == PT_STATEREP)
            return ERROR_INVALID_DATA;

        if (Type == PT_NEWEFREP) {
            // Special case for new effect, ID is third byte
            *effectId = packet->data[2];
            return ERROR_SUCCESS;
        }
        // The Effect Block Index is the second byte (After the Report ID)
        *effectId = packet->data[1];

4) TrigerBtn is specified in Microsoft.DirectX.DirectInput: https://docs.microsoft.com/en-us/previous-versions/ms835075(v=msdn.10) ...I think it can be seen somewhere in SlimDX. It is not button number, but a series of flags like this Visual Basic pseudocode: effect.TriggerButton = CInt(Microsoft.DirectX.DirectInput.Button.NoTrigger)

cyberluke commented 1 year ago

BTW: Some files have coding UTF-8 (with BOM signature), some files CP1252. Line endings seems to be CR LF (Windows style).

But Visual Studio 2015 shows popup after each build on few specific files about inconsistent line endings. This can be solved with Advanced Save Options (You need to add it via Tools -> Customize -> Commands -> Add Command). This is per file settings. As of 2022 there is no per project settings. There are some community made add-ins for VS.

Would be good to get some developer codestyle guide for pull requests.

cyberluke commented 1 year ago

Initial commit with slightly hacked up solution just to get to a working state is here: https://github.com/erik-smit/FreePIE/compare/Ffb...cyberluke:FreePIE:Ffb

Current fork is here: https://github.com/cyberluke/FreePIE/tree/Ffb

What is working:

Additional changes:

ffb This timeline gets played and exactly reproduced

Issues:

Current plans:

It would be great if whole FFB packet could be re-send via this method: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee417917(v=vs.85)

cyberluke commented 1 year ago

It should be possible to route directly, first setDataFormat tu custom format: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee416633(v=vs.85) ...set it to DIEFFECT structure: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee416616(v=vs.85) ...then call SendDeviceData() ...I think this is the main use case here. Nobody will mess with individual CreateEffect stuff in FreePIE. Everyone want to map axis, button and FFB effect needs 1:1 mapping (just route them and make it quick)

AndersMalmgren commented 1 year ago

@cyberluke i think it might be better to upgrade to 4.8 instead since 4.5.x isn't supported by VS 2022 without installing that runtime.

cyberluke commented 1 year ago

You said guys still compile it on Visual Studio 2010 (in another thread, I think). I'm using Visual Studio 2015 on Windows 10.

I'm definitely not upgrading in Force Feedback pull request, there's no need to. It can be handled separately. One step at a time.

Also during first COVID lockdown, when I joined retrogaming community and bought several computers, I learnt that it might be better to choose the most compatible solution for a broad spectrum of use cases. Of course commercial & Microsoft will always throw stuff and make it pain. But I learnt to not play that kind of game. Before I was the bleeding edge guy. Even DirectX is not supported anymore, right? And SlimDX is like DirectX 8 interface with DirectX 9 runtime. This is not supported by design in 2022 :-D. If the solution for VS 2022 involves only installing some runtime (I would say it is SDK not runtime), then it's not an issue. You also need to install Windows XP backporting feature in Visual Studio setup, for example.

When you start digging into built-in FreePIE libraries, such as VJoy or SlimDX, you will find out you need to download a lot of additional SDK's (like DirectX 9 SDK, 10 SDK, 11 SDK) no matter what IDE you use. And I had to do it to fix some bugs or debug something or just read comments from external library.

AndersMalmgren commented 1 year ago

You said guys still compile it on Visual Studio 2010 (in another thread, I think). I'm using Visual Studio 2015 on Windows 10.

I'm definitely not upgrading in Force Feedback pull request, there's no need to. It can be handled separately. One step at a time.

Also during first COVID lockdown, when I joined retrogaming community and bought several computers, I learnt that it might be better to choose the most compatible solution for a broad spectrum of use cases. Of course commercial & Microsoft will always throw stuff and make it pain. But I learnt to not play that kind of game. Before I was the bleeding edge guy. Even DirectX is not supported anymore, right? And SlimDX is like DirectX 8 interface with DirectX 9 runtime. This is not supported by design in 2022 :-D. If the solution for VS 2022 involves only installing some runtime (I would say it is SDK not runtime), then it's not an issue. You also need to install Windows XP backporting feature in Visual Studio setup, for example.

When you start digging into built-in FreePIE libraries, such as VJoy or SlimDX, you will find out you need to download a lot of additional SDK's (like DirectX 9 SDK, 10 SDK, 11 SDK) no matter what IDE you use. And I had to do it to fix some bugs or debug something or just read comments from external library.

If you look at the msbuild files to build the installer you can see it says tool version. Which indeed was introduced in 2010

AndersMalmgren commented 1 year ago

Tried to update to 4.8 and build it on tools version 4 and it seems to work. It build correctly.

Just tried quickly to use the keyboard plugin and it reads keys correctly under that runtime. It uses slimdx if I recall correctly

cyberluke commented 1 year ago

What concerns me more now is if it is possible to mark external library Enum with [GlobalEnum], so FreePIE can see it and use it.

using FreePIE.Core.Contracts;

namespace FreePIE.Core.Plugins.VJoy
{
    [GlobalEnum]
AndersMalmgren commented 1 year ago

What concerns me more now is if it is possible to mark external library Enum with [GlobalEnum], so FreePIE can see it and use it.

using FreePIE.Core.Contracts;

namespace FreePIE.Core.Plugins.VJoy
{
    [GlobalEnum]

Freepie supports external plugins. You just reference FreePIE.Core.Contracts and use the attributes. You can build your custom plugin and place it in the plugins folder. Though i think this should be part of the core plugins

cyberluke commented 1 year ago

Perfect, I think there is some info on wiki, but must have missed this particular one.

All FFB packets and protocol features are implemented now: https://github.com/cyberluke/FreePIE/commit/0f0ba136e05b5ecabea20f981757ae759a607af7

Some effects throw E_INVALIDARG because some parameters are not properly converted. VJoy sends byte, uint or short on the output, but SlimDX always requires int parameters. Will need to dig into SlimDX sources and tests to figure out the exact format and range of each parameter.

Then the next real test can happen (real game) and this will show the performance issues.

cyberluke commented 1 year ago

Fixed a few parameters. Everything except Ramp seems to be working (ignoring that for now as it might be the well known SlimDX issue).

Mig 21 Fulcrum on Logitech G940 flight stick now fully works (including throttle and pedals). Just must not shoot otherwise it creates Ramp effect. Take off have vibration, spring feedback provides air force feedback. No exceptions in the console. Some packets have latency only 9ms. But some packets have delay 300ms. So it is unplayable yet.

cyberluke commented 1 year ago

One thing that bothers me in FreePIE: Even you disable force feedback and have only simple Vjoy to SlimDX mapping script. The latency is already somewhere between 20-50ms even with Release build and setting Process Thread to Realtime in Windows Task Manager.

I think the next step would be to try Logitech SDK and compare it with SlimDX latency.


if starting:
    G940_JoystickID=3;
    G940_PedalsID=2;
    G940_ThrottleID=0;

    #vJoy[0].registerFfbDevice(joystick[G940_JoystickID])
    diagnostics.debug("starting")

    force = 0

    import time
    import math

    #The following is the tweakaxis function. Don't change this unless you know how to program. Call on this function in the Axis Rebinds section.
    def tweakaxis(input, input_range_min, adjusted_center, input_range_max, dband):
        if input >= adjusted_center:
            return filters.ensureMapRange(input, adjusted_center + dband, input_range_max, 0, 1000)
        elif input <= adjusted_center:
            return filters.ensureMapRange(input, input_range_min, adjusted_center - dband, -1000, 0)
        else:
            return 0
    #The tweakaxis function's variables explained:
    #input:-------------This specifies the input (usually a real joystick axis) to be modified.
    #input_range_min:---The value of the input to map to the tweaked output's full minimum. Use -1000 for an unchanged endpoint on conventional joysticks.
    #adjusted_center:---The value of the input to map to the tweaked output's center. Assign a new center point to your joystick's axis with this value.
    #input_range_max:---The value of the input to map to the tweaked output's full maximum. Use 1000 for an unchanged endpoint on conventional joysticks.
    #dband:-------------Deadband. The distance that you must move the input away from adjusted_center before the smallest value is output.
    #Tips:
    #input_range_min and _max can be changed to alter the effective range of an axis on a physical device.
        #For example, it can be used to halve the stick travel distance for the full range of outputs by using a _min of -500 and a _max of 500.
        #If making asymmetrical changes to the input_range values, remember to consider whether adjusted_center needs to bisect those values.
        #Using values for these vars with absolute value greater than 1000 (again, for most sticks) will result in more precision by sacrificing access to the output values at the extremities.
    #tweakaxis() is meant to be composed, including with itself. The order matters greatly. The true power of the script lies here.

    #Enclose every set of functions in this before setting a vJoy axis equal to it:
    def vJR(input):
        return filters.ensureMapRange(input, -1000, 1000, -vJoy[0].axisMax, vJoy[0].axisMax)

    #Curve functions:
    #Basic x^n curve:
    def powcv(input, curvepower, min = -1000, max = 1000):
        if input >= 0:
            return filters.ensureMapRange(math.pow(input, curvepower), 0, math.pow(max, curvepower), 0, max)
        else:
            return filters.ensureMapRange(-math.pow(-input, curvepower), -math.pow(-min, curvepower), 0, min, 0)

    #Fancy -((1-x)^n)+1 curve, made origin-symmetric:
        #Use 0 << n << 1 for best results, with small f''(x) for small x and large f''(x) for large x.
    def revrootcv(input, curvepower, min = -1000, max = 1000):
        if input >= 0:
            return filters.ensureMapRange(1-math.pow(1-filters.ensureMapRange(input, 0, max, 0, 1), curvepower), 0, 1, 0, max)
        else:
            return filters.ensureMapRange(-1+math.pow(1-filters.ensureMapRange(-input, 0, -min, 0, 1), curvepower), -1, 0, min, 0)

    #Master-controlled scaling for inputs:
        #The variable limits created converge on the center of whichever function is directly inside it.
        #"master" switch made to easily determine which control to use.
    def mscale(input, master = 0):
        if master == 0: #Positional master
            scalevalue = filters.ensureMapRange(-joystick[2].x, 0, 1000, 0.15, 1)
            if joystick[2].x > 990:
                scalevalue = 0
            return input * scalevalue
        elif master == 1: #Rotational master
            scalevalue = filters.ensureMapRange(joystick[2].zRotation, -990, 990, 0, 1)
            return input * scalevalue
        else:
            return 0
    #Change axis_mult if 'Windows output: vJoy' axes have the wrong range of motion.
    #   (They should match the correspondingly bound values of
    #   'Windows output: Physical' 1 and 2 exactly.)
    axis_mult = (vJoy[0].axisMax / 1000)

t0 = time.clock()

#Joystick button to keyboard button variable function
def jkey(j_index, j_button, keybind):
    if joystick[j_index].getDown(j_button):
        keyboard.setKeyDown(keybind)
    else:
        keyboard.setKeyUp(keybind)

#Joystick button to keyboard button variable function
def keyj(Key, j_button):
    if keyboard.getKeyDown(Key):
        vJoy[0].setButton(j_button, 1)
    else:
        vJoy[0].setButton(j_button, 0)

def keyCodeToNumber(key):
    if (key == Key.D1):
        return -16535
    if (key == Key.D2):
        return -12258        
    if (key == Key.D3):
        return -8192        
    if (key == Key.D4):
        return -4096        
    if (key == Key.D5):
        return 0        
    if (key == Key.D6):
        return 4096       
    if (key == Key.D7):
        return 8192       
    if (key == Key.D8):
        return 12258        
    if (key == Key.D9):
        return 16535

#Joystick button to keyboard button variable function
def keyaxis(key):
    if keyboard.getKeyDown(key):
        vJoy[0].z = keyCodeToNumber(key)

#Joystick button to vJoy button variable function
def jvjb(j_index, j_button, vjoy_button):
    if joystick[j_index].getDown(j_button):
        vJoy[0].setButton(vjoy_button, 1)
    else:
        vJoy[0].setButton(vjoy_button, 0)

#Axis rebinds
#Adding 1 before multiplying by axis_mult seems to compensate for
#   a one step offset in the vJoy driver itself.
jvjb(G940_JoystickID, 0, 0)
jvjb(G940_JoystickID, 1, 1)
jvjb(G940_JoystickID, 2, 2)
jvjb(G940_JoystickID, 3, 3)
jvjb(G940_JoystickID, 4, 4)
jvjb(G940_JoystickID, 5, 5)
jvjb(G940_JoystickID, 6, 6)
jvjb(G940_JoystickID, 7, 7)
vJoy[0].setAnalogPov(0, joystick[G940_JoystickID].pov[0])
vJoy[0].rx = 10*joystick[G940_JoystickID].xRotation;
vJoy[0].rz = 10*joystick[G940_PedalsID].zRotation;
#vJoy[0].slider = 10*joystick[G940_ThrottleID].x;
vJoy[0].x = 10*joystick[G940_JoystickID].x;
vJoy[0].y = 10*joystick[G940_JoystickID].y;
vJoy[0].z = 10*joystick[G940_JoystickID].z;

diagnostics.watch(force)

if(force < 10000):
    force += 10
else:
    force = -10000

#if(force % 100 == 0):    
#    joystick[G940_JoystickID].operateEffect(0, EffectOperation.Start, 0)

#joystick[G940_JoystickID].setConstantForce(0, 5000)
#joystick[G940_JoystickID].operateEffect(0, EffectOperation.Start, 0)

diagnostics.watch(joystick[G940_PedalsID].zRotation)
diagnostics.watch(joystick[G940_ThrottleID].x)
#Script execution time tracker
totaltime = time.clock() - t0
diagnostics.watch(totaltime)
""""""
cyberluke commented 1 year ago

Now this looks cool: https://www.mtbs3d.com/phpBB/viewtopic.php?f=139&t=18724

AndersMalmgren commented 1 year ago

Now this looks cool: https://www.mtbs3d.com/phpBB/viewtopic.php?f=139&t=18724

I don't think you want todo sensitive timing things on the script thread. Its defaulted to 64hz (16ms).

You could have a thread going on in the background in the plugin for example, the Serial com plugin does that for example. Though if you don't have any I/O there will be no interupts and the thread will use all resources on that CPU core.

cyberluke commented 1 year ago

For now increasing the timing to 500hz did help and even vjoy latency without force feedback is much better. Thank you very much for this valuable information, I will look at it in the future!

cyberluke commented 1 year ago

That ComDevicePlugin did not help. I am facing another situation. Both VJoy and SlimDX only register their global handlers and there is a lot of static methods as well.

The current latency with System Timer is between 3ms and 9ms, which is great. There is occasional spike to 20ms or even 300ms.

But in SlimDX docs there is mentioned that FFB processing must be fast because it is blocking operation.

There will be some basic logic mistake somewhere in current usage of VJoy and SlimDX libraries. Even 9ms of FFB packet processing will cause stuttering to the game (even with keyboard control). When game stops sending FFB packets, there is no stuttering.

This is the last thing to solve (plus Ramp effect params tweak).

cyberluke commented 1 year ago

I need to refresh my C# skills as I work in Java for the last 5 years. But something like this helps to unblock the processing, but now the DirectX device will not receive FFB information. The packets are parsed and processed and written out to console though. It will probably need some kind of synchronized block when working with Device class.

This code processes the FFB packet and response in-game is smooth. It will just not get delivered to SlimDX Device registered in Global handlers, probably due to some threading issues. No exception though.


        /// <summary>
        /// Called when vJoy has a new FFB packet.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="userData"></param>
        private static void OnFfbPacketAvailable(IntPtr data, IntPtr userData)
        {
            Task.Run<String>(async () =>
            {
                FfbPacket ffbPacket = new FfbPacket(data);
                var pa = packetMapper[ffbPacket.PacketType];
                if (pa != null)
                    queueWrapper.Add(pa.Convert(ffbPacket));

                await Task.Yield();
                return null;
            });
        }
cyberluke commented 1 year ago

With Task.Run, some packets that come at the same time will get lost. Tried to add lock(Device) {} block, but that does not help.

But If I keep adjusting parameters, it gets received by some time and force feedback will work. Just need to somehow manage to not keep overwriting packets. Perhaps the data pointer IntPtr gets changed in the middle of creating the FfbPacket object.

cyberluke commented 1 year ago

I have new commit here: https://github.com/cyberluke/FreePIE/commit/65775eb80e4e5e4c84a197385dfa7282372073fa

So the issue above with C++ pointers has been confirmed.

Analysis: 1) Incoming callback use the same IntPtr all over again rewriting the data (like circular buffer) 2) Any delay in C# callback of VJoy means Game UI is blocked - it is blocking operation!!! Instant FPS kill.

Partially Failed Solution: 1) The only operation you want to do in C# callback is to copy IntPtr to your own memory location, so next packet will not rewrite it and then pararelize it on background (saving to blocking queue, so the order should be the same)

But it looks like this and InternalFfbPacket itself is IntPtr. So you have to deal with pointer and its nested pointer in order to do it properly!

        [StructLayout(LayoutKind.Sequential)]
        public struct InternalFfbPacket
        {
            public int DataSize;
            public CommandType Command;
            public IntPtr PtrToData;
        }

At this time I thought I'm lucky to know so many programming languages...

So in callback you want to do only this:

        public FfbPacket(IntPtr packetPtr)
        {
            //copy ffb packet to managed structure
            packet = (InternalFfbPacket)Marshal.PtrToStructure(packetPtr, typeof(InternalFfbPacket));

            _Data = new byte[packet.DataSize];
            Marshal.Copy(packet.PtrToData, _Data, 0, (Int32)packet.DataSize);

            // Convert object to pointer
            _DataPtr = GCHandle.Alloc(_Data, GCHandleType.Pinned);
            packet.PtrToData = _DataPtr.AddrOfPinnedObject();
            packetPtrCopy = Marshal.AllocHGlobal(Marshal.SizeOf(packet));
            Marshal.StructureToPtr(packet, packetPtrCopy, false);
        }

Of course you need to have Destructor as well:

        protected void Free()
        {
            Marshal.FreeHGlobal(packetPtrCopy);
            packetPtrCopy = IntPtr.Zero;
            _DataPtr.Free();
        }

3) Now everything running smoothly, UI is not blocked and packet content is not randomly rewritten. But creating of a pinned object in memory in C# is not that fast. So now the game is smooth with non-blocking operation, but the latency have increased from 3-9ms before to 100-300ms now. Therefore it is probably time to dig into VJoy C++ source and fix it natively from there. There should be some method to clone the packet in memory and then free it.

Because this sequential solution of packets processing in C# is not fast and is blocking and cannot be paralelized easily. If you would go with ARM or ATMEL microcontroller and do the processing there, it would be much faster than in C#.

This is so painful branch to work with :-D one bottleneck next to each other. One syntax error next to each other. I take it back. The generics and async processing is useless and not working. It's 3AM again and I need to work more...

cyberluke commented 1 year ago

So I fixed this branch in C#, but now I need to rewrite everything in C++ because callback and rerouting of packets will be always bottleneck here :-/

cyberluke commented 1 year ago

Meanwhile there is pull request to fix two VJoy bugs. First is Gain Packet is always zero due to wrong type cast. Second is a mistake in helper class, which returns always 2 bytes only (but it is not used in this branch, so only Gain packet is issue).

That's why in this branch I always set min. gain to 5000: https://github.com/njz3/vJoy/pull/3

cyberluke commented 1 year ago

Updated FFB fork: https://github.com/cyberluke/FreePIE/tree/Ffb

So Erik removed Marjin's AsyncActionRunner because of some memory exceptions. These memory exceptions were happening because FFB callback provides Int Pointer to FFB packet, but this gets rewritten by the next packet. Marjin had some unsafe FFB branch in his fork, but the marshalling is not correct. He forgot to work with the nested Int Pointer IntPtrData inside FFB packet. Also it was slow.

So now FFBPacket constructor has been fixed and split to two parts. First starts in sync callback. Second is for async lazy load to improve latency.

        public FfbPacket(IntPtr packetPtr)
        {
            ClonePacket(packetPtr);
        }

        public void ClonePacket(IntPtr data)
        {
            unsafe
            {
                InternalFfbPacket* FfbData = (InternalFfbPacket*)data;
                int size = FfbData->DataSize;
                int command = (int)FfbData->Command;
                byte* bytes = (byte*)FfbData->PtrToData;
                inMemoryPacket = new InternalFfbPacket();
                inMemoryPacket.DataSize = size;
                inMemoryPacket.Command = FfbData->Command;
                newData = new byte[size];
                Marshal.Copy(FfbData->PtrToData, newData, 0, (Int32)size);
                FFBPType type = FFBPType.PT_STATEREP;            
                VJoyUtils.Joystick.Ffb_h_Type(data, ref type);
                PacketType = type;
            }
        }

        public void Init() {
            _DataPtr = GCHandle.Alloc(newData, GCHandleType.Pinned);
            inMemoryPacket.PtrToData = _DataPtr.AddrOfPinnedObject();
            _PacketPtr = GCHandle.Alloc(inMemoryPacket, GCHandleType.Pinned);
            packetPtrCopy = _PacketPtr.AddrOfPinnedObject();

            //Read out the first two bytes (into the base packetData class), so we can fill out the 'important' information
            uint effectId = 0;
            VJoyUtils.Joystick.Ffb_h_EffectBlockIndex(packetPtrCopy, ref effectId);
            BlockIndex = (int) effectId;

            uint deviceId = 0;
            VJoyUtils.Joystick.Ffb_h_DeviceID(packetPtrCopy, ref deviceId);
            DeviceId = (int)deviceId;
            if (DeviceId < 1 || DeviceId > 16)
                throw new Exception(string.Format("DeviceID out of range: {0} (should be inbetween 1 and 16)", DeviceId));

        }

In FFB callback we call FFBPacket constructor to only quickly clone data bytes into our own property without creating any C++ pointer. Plus we read Packet Type to optimize further processing decision logic.

In Init() method is the code to lazily initialize C++ pointer to our managed C# data, so underlying VJoy C++ library (C# wrapper) can be used. This happens in async method and it does not block FFB callback anymore.

This results in smooth gameplay, game UI thread does not stutter anymore. Latency delay of FFB packets is between 9-30ms with spikes of 300ms. This results in bad experience as force feedback is delayed and does not correspond to the game. This is without AsyncActionRunner.

Further addition of AsyncActionRunner only slows it down. Which is strange. The performance is worse than single thread. With incoming packets, the delay keeps increasing up to 20 seconds. It queues all packets quickly, nothing is lost. But then the actual processing of individual packets sequentially bottlenecks each other packet increasing delay. I tried to optimize it by allowing some packets to be processed in parallel and trying to discard possible duplicate packets, but that does not solve the issue.

Currently I need to implement the same solution only in C++ in order to compare the speed. This is the big issue now. It is working, but due to creating new SlimDX Effect object each packet (like 50 packets in 5ms), it kills the gaming experince.

cyberluke commented 1 year ago

Some further hints: https://www.nuget.org/packages/Microsoft.IO.RecyclableMemoryStream/ https://adamsitnik.com/Array-Pool/ (this is like the FlyWeight design pattern I discussed before) https://stackoverflow.com/questions/63363438/copying-pointers-data-to-byte-array-and-writing-to-memorystream-results-to-tons

This would help only with blocking GC call, but there are more performance issues on the road.

cyberluke commented 1 year ago

4:30 AM time for status. First lets have a look at this picture -->

solved

Process delay: 0ms

Looks like it is not working? But it is working! How? Cyberluke smashed keyboard randomly providing a series of illegal operations as well as random code blocks in memory deletion. Also had to write a little bit different VJoy C++ API.

Next time...try harder

cyberluke commented 1 year ago

Probably solved Ramp Effect. There is a bug in SlimDX as several people on the internet thought. But nobody try to open DirectX SDK help or look at the SlimDX class.

So here is nice DirectX 9 page explaining even parameter range: http://doc.51windows.net/Directx9_SDK/input/ref/structs/dirampforce.htm

SlimDX and VJoy use int instead of long, but that should be ok as the range is from 0 to 10000. EDIT: the above link have incorrect character. Correct range is -10 000 to 10 000. You need to really download that Direct9 SDK and open *.chm help file included.

typedef struct DIRAMPFORCE {
    LONG  lStart;
    LONG  lEnd;
} DIRAMPFORCE, *LPDIRAMPFORCE;
typedef const DIRAMPFORCE *LPCDIRAMPFORCE;

This is SlimDX implementation:

    int RampForce::Size::get()
    {
        return sizeof( DICONSTANTFORCE );
    }

    void *RampForce::ToUnmanaged()
    {
        // Manual Allocation: released in Release function
        DIRAMPFORCE *result = new DIRAMPFORCE();

        result->lStart = Start;
        result->lEnd = End;

        return result;
    }

If you compare RampForce.cpp with ConstantForce.cpp, you see that RampForce should return:

    int RampForce::Size::get()
    {
        return sizeof( DIRAMPFORCE );
    }

This is probably some copy&paste error. Now I need to download three versions of DirectX SDKs, but my hybrid multiboot WinXp / Win10 machine is out of space, so I have to probably boot to Hirens Boot CD and change partition order then change partition size. Which means: backup data now :-)

cyberluke commented 1 year ago

Few notes for @AndersMalmgren why not to move to latest .NET. In Czech, we call it "salamova metoda" or ham method. Each year they will push you a little with passive agressivity and you loose something. This year it is .NET 4.7. Next year, there will be only .NET 5.0, then 6.0. And voila! From superb project that can be used for many years, you will get Windows 11 or Windows 12 only app. Users need to buy new Windows to get legacy app running. It is a proven Apple and Microsoft business model.

Back to SlimDX: Recompiled SlimDX in Windows XP (Visual Studio 2010). This is the reason why you don't want to bump artificially some SDK version like .NET 4 to 4.7! Why?

Because Windows 10 removed DirectX SDK - we know that.

And yes, it looks quite easy to just pull DX9 and XAudio headers via Nuget package. Plus installing XAudio2 runtime. Ok. But it still won't compile.

And guess what? It won't compile even as C++ package without .NET!

The reason is that Microsoft constantly keeps changing even old deprecated interfaces on a purpose. They just want to break compatibility on purpose to tell you: "You need to buy new computer!" Here is specific SlimDX issue: They completely changed several XAudio classes for Windows 10 SDK, even it is stable/deprecated.

They backported breaking changes.

Same as VCREDIST_X86 (Visual Studio 2015 C++ Runtime) - it should run on Windows XP and always did.

But THIS YEAR Microsoft removed a lot of DLLs on purpose even from 2010, 2015 C++ runtimes to remove WXP.

So it means, you install it from official MS website on WXP, but it still won't work! And you don't know why. Then you download VCREDIST_X86 from 2021, available only on Archive.org, and it start to work on Windows XP!!! It work always till few months recently they start removing DLLs even from outdated installers hosted on their website. They claim that SHA-1 signature is not supported anymore. But that does not mean they cannot host old files.

They just remove legacy download completely. So you will not find Windows 7.1 DDK (driver development kit) for VJoy, for example.

So on Windows XP, SlimDX compiles out of the box. On Windows 10, you would need to read ten pages on fan blog and REWRITE XAudio calls and parameters. This would on purpose only break Win XP, Win Vista, Win7 compatibility. While, if you not lazy developer, and you do your job properly, you will compile it on Windows XP (or Windows 7) and it will run on all OSes, including Win 10.

SlimDX: FIXED, they have pull request here: https://github.com/SlimDX/slimdx/pull/515

Compiled Release DLL x86 is already in FreePIE. Same as new VJoy.

cyberluke commented 1 year ago

ramp

Ramp Effect working. Including parameters.

Tweaked also a few other parameters. It runs much faster if you disable Console.WriteLine() calls.

                Console.SetOut(System.IO.TextWriter.Null);
                Console.SetError(System.IO.TextWriter.Null);

https://github.com/cyberluke/FreePIE/commit/1ba70494023c5f282a799d689668d655783be175

cyberluke commented 1 year ago

The Spring Effect force is not perfect yet. Sometimes it is delayed and jumps in force.

1) After reading DirectX 9 SDK docs, each device reports what effects and what parameters it supports. At FreePIE VJoy start, it should query DirectInput (SlimDX) device for parameters and send them to VJoy, so VJoy can report it to the game and game can send correct parameter values and range.

2) After analyzing logs, there is a lot of repeating calls. There should be optimization using some new PacketFilter class. This class will have different strategy per packet type. It will save last byte data for each packet type. And if exactly same bytes come multiple times, they get ignored, thus improving packet throughput. This way same effect with exactly same parameters will not get recreated. But on the other hand START, STOP and BLCKFREE command will always execute as you can replay already uploaded effect in device (furthermore START should always execute if effect duration != infinite).

3) And there will be a pool of object instances for SlimDX Effect objects (and EffectParameters objects). Because C# is not good for this. You create like 100 objects in half second and what it will do: It will run Garbage Collector, which completely blocks all threads, including game, causing 200ms spike we can still see. If we keep rotating lets say an array of 10 Effect instances without creating new instances, GC will not even start. You have C# bells and whistles, but you need to type more code and optimize object creation only to get a simple algorithm running.

AndersMalmgren commented 1 year ago

@cyberluke I dont have time to maintain FreePIE, I put all my free time into a VR game project. https://www.youtube.com/c/MDADigital

Moving the GUI to .NET 6 shouldnt be a problem. The problem is that there is alot of Windows specific code pinvoke etc. I guess its possible with additional packages to get Windows speciifc features from full framework in .NET 6, I havent investigated what it takes.

            Console.SetOut(System.IO.TextWriter.Null);
            Console.SetError(System.IO.TextWriter.Null);

That disables output to the console. Which you dont want. Only write importanta stuff to the console. I think its mosly used to write warnings to the Error panel like

            if (!match)
                Console.Error.WriteLine("vJoy version of Driver ({0:X}) does NOT match DLL Version ({1:X})", driverVersion, apiVersion); 

Only use Console.WriteLine during development. When going live throw errors for the script to stop or Console.Error.WriteLine for warning

CyberLuke-GBG commented 1 year ago

Nice, I was making VR controllers and was on CES 2016 in LV. Here: https://www.facebook.com/realmagicvr/ and here: https://futurezone.at/digital-life/real-magic-haende-sollen-als-vr-controller-dienen/264.283.726 and smart VR shoe: https://www.youtube.com/watch?v=1fNNxPqh-S8&list=UUT7Ad5kcEYETdI914zGOKuQ ...the VR shoe would be able to compute rotation and relative position as well as precise pressure. Then there could be a kind of pressure based floor in VR arena for absolute position without camera. But it did not make any money and now I work as enterprise Java integration tech lead.

If you don't want to be a mantainer, I can help, at least for some time. It would be my pleasure! But I would preserve, not introduce any breaking changes because I think that is silly and not necessary.

You know, you don't understand: Nobody want .NET 6. Why to kill compatibility. After COVID retrogaming movement is raising. Win 11 will always run older .NET software, so it will stay compatible. Now it can still run on Windows XP. I buy a lot of Steam games, remakes, and GoG games. And they do not work on Windows 10. Like Mig-29 Fulcrum I am testing now. It does not work in fullscreen properly. Aquanox have issues with EAX sound, even I have three different Sound Blaster soundcards. So I have dual boot to WinXp and Win 10. On top of that I have 10 computers here including Voodoo 2 SLI and Pentium MMX.

cyberluke commented 1 year ago
  1. added method diagnostics.enable() & disable() for turning off console -> handy for switching from performance testing to debug during one session => https://github.com/cyberluke/FreePIE/commit/bdb8fc46269bdbce7b1ab57357afe483a6d73541
  2. Console.WriteLine(something.ToString()) => this gets still evaluated and toString() methods do have performance impact => in my code, I have surrounded it with #if DEBUG and #endif directives, so it is removed during compile time if DEBUG directive is not set.
AndersMalmgren commented 1 year ago
  1. added method diagnostics.enable() & disable() for turning off console -> handy for switching from performance testing to debug during one session => cyberluke@bdb8fc4
  2. Console.WriteLine(something.ToString()) => this gets still evaluated and toString() methods do have performance impact => in my code, I have surrounded it with #if DEBUG and #endif directives, so it is removed during compile time if DEBUG directive is not set.

I'm not sure I like that, We shouldnt have any Console.WriteLines in the plugins once they are developed. Only one that should be writing to the console is the scripts using diagnostics.debug

cyberluke commented 1 year ago

I agree, but it was like that when I came here. On the other hand there will be always need to retest a code, so there needs to be something. I can get it outside of PR and use it only in my fork and focus on my fork only to get the job done.

AndersMalmgren commented 1 year ago

I agree, but it was like that when I came here. On the other hand there will be always need to retest a code, so there needs to be something. I can get it outside of PR and use it only in my fork and focus on my fork only to get the job done.

I'm of the school that you add such logging when you need it and remove it when you are done. Plus break points are better 9 times out of 10.
I checked the code and the only writelines we have today is in the vjoy plugin and they output to the error panel (warning about version diff).

But yeah like you said. I think it should be in another PR.

cyberluke commented 1 year ago

If you get 5000 packets in 2 minutes, you really really do not want to set a breakpoint :-) sometimes you need to: 1) analyze protocol 2) profile

Debugger with breakpoint is if you looking for a specific exception or value of single element. Realtime communication is not that case.

I will not be adding and removing such logging every month.

Today, I look at several projects that use tracing and debug. They use pragma directive for compiler, so I've chosen that approach.

I respect your opinion, but I think it is not practical. It depends what kind of functionality are you developing. In VR you also cannot set breakpoint and wear helmet.

No, VJoy prints a lot of stuff to console. You can see it here in this PR, in the screenshots. It is not only warning and errors.

So I might remove diagnostics.enable() and diagnostics.disable() and continue working on my fork. You said you don't have time to mantain it here anyway. But I am keeping Console.WriteLine() and compiler directives in my code, namely #if DEBUG and #endif. The only thing I might change is the name 'DEBUG' to something else. I can change it to 'TRACE_ENABLE' for example because these are tracing outputs. That is commonly used in software development.

AndersMalmgren commented 1 year ago

If you get 5000 packets in 2 minutes, you really really do not want to set a breakpoint :-) sometimes you need to:

  1. analyze protocol
  2. profile

Debugger with breakpoint is if you looking for a specific exception or value of single element. Realtime communication is not that case.

I will not be adding and removing such logging every month.

Today, I look at several projects that use tracing and debug. They use pragma directive for compiler, so I've chosen that approach.

I respect your opinion, but I think it is not practical. It depends what kind of functionality are you developing. In VR you also cannot set breakpoint and wear helmet.

No, VJoy prints a lot of stuff to console. You can see it here in this PR, in the screenshots. It is not only warning and errors.

So I might remove diagnostics.enable() and diagnostics.disable() and continue working on my fork. You said you don't have time to mantain it here anyway. But I am keeping Console.WriteLine() and compiler directives in my code, namely #if DEBUG and #endif. The only thing I might change is the name 'DEBUG' to something else. I can change it to 'TRACE_ENABLE' for example because these are tracing outputs. That is commonly used in software development.

Vjoy plugin does not print to console in master. Must be debug code in that branch. Spraying with if debug statement will make code less readable. It's fine doing under development and even ok to check in under dev branch. Put i wouldn't accept a pull to master without debug code removed

cyberluke commented 1 year ago

Even VJoy kernel driver have this in source.

It is not if debug statement. It is compiler directive. That is something else.

I try to be helpful here and provide maximum verbosity, but I think our professional opinions will differ even more soon.

Thank you, I will not be doing any pull request then.

Have a nice day and good luck with your VR project.