[C#] Should OverlayElementFactory be abstract?

Problems building or running the engine, queries about how to use features etc.
Post Reply
dunklemateria
Gnoblar
Posts: 7
Joined: Sun May 16, 2021 8:11 am

[C#] Should OverlayElementFactory be abstract?

Post by dunklemateria »

Hello,

as I wrote in my first thread, registering custom overlay element factories is now possible. Because of org.ogre.OverlayElementFactory is not abstract and does not contain a constructor, it results in compiler error https://docs.microsoft.com/en-us/dotnet ... Dk(CS1729) when I try to extend a CustomOverlayElementFactory from it. So I implemented my factory by extending org.ogre.PanelOverlayElementFactory. But this does not work either, because the getTypeName() method is not overridden and log shows multiple registrations of "Panel" as type name:

Code: Select all

....
03:14:54: *-*-* Version 1.12.12 (Rhagorthua)
03:14:54: OverlayElementFactory for type Panel registered.
03:14:54: OverlayElementFactory for type BorderPanel registered.
03:14:54: OverlayElementFactory for type TextArea registered.
03:14:54: Registering ResourceManager for type Font
....
03:14:54: Creating resources for group OgreInternal
03:14:54: All done
03:14:54: D3D9 : RenderSystem Option: FSAA = 0
03:14:54: D3D9 : RenderSystem Option: Full Screen = No
03:14:54: D3D9 : RenderSystem Option: Video Mode = 1024 x 768 @ 32-bit colour
03:14:54: OverlayElementFactory for type Panel registered.
03:14:54: OverlayElementFactory for type Panel registered.
03:14:54: OverlayElementFactory for type Panel registered.
....
I also tried to create an abstract class in my code and derive from that. In case of extending directly OverlayElementFactory, compiler error CS1729 appear. Extending PanelOverlayElementFactory same behaviour as described above, factories are registers all with the same type name. It is easily reproducable with this code:

Code: Select all

using System;
using org.ogre;

namespace OgreTest
{
    public class KeyListener : InputListener
    {
        ApplicationContext ctx;

        public KeyListener(ApplicationContext ctx)
        {
            this.ctx = ctx;
        }

        public override bool keyPressed(KeyboardEvent evt)
        {
            if (evt.keysym.sym == 27)
                ctx.getRoot().queueEndRendering();
            return true;
        }
    }

    public class CustomOverlayElement : PanelOverlayElement
    {
        public CustomOverlayElement(string instanceName) : base(instanceName)
        {
        }
    }

    public class CustomOverlayFactory : PanelOverlayElementFactory
    {
        public override OverlayElement createOverlayElement(string instanceName)
        {
            return new CustomOverlayElement(instanceName);
        }

        public override string getTypeName()
        {
            return "Custom";
        }
    }

    public class OverlayFactoriesExample : ApplicationContext
    {
        InputListener listener;

        public OverlayFactoriesExample()
        {
            listener = new KeyListener(this);
        }

        public override void setup()
        {
            base.setup();
            addInputListener(listener);

            var root = getRoot();
            var scnMgr = root.createSceneManager();

            var shadergen = ShaderGenerator.getSingleton();
            shadergen.addSceneManager(scnMgr); // must be done before we do anything with the scene

            scnMgr.setAmbientLight(new ColourValue(.1f, .1f, .1f));

            var light = scnMgr.createLight("MainLight");
            var lightnode = scnMgr.getRootSceneNode().createChildSceneNode();
            lightnode.setPosition(0f, 10f, 15f);
            lightnode.attachObject(light);

            var cam = scnMgr.createCamera("myCam");
            cam.setAutoAspectRatio(true);
            cam.setNearClipDistance(5);
            var camnode = scnMgr.getRootSceneNode().createChildSceneNode();
            camnode.attachObject(cam);

            var camman = new CameraMan(camnode);
            camman.setStyle(CameraStyle.CS_ORBIT);
            camman.setYawPitchDist(new Radian(0), new Radian(0.3f), 15f);
            addInputListener(camman);

            var vp = getRenderWindow().addViewport(cam);
            vp.setBackgroundColour(new ColourValue(.3f, .3f, .3f));

            var fac = new CustomOverlayFactory();
            GC.SuppressFinalize(fac);
            OverlayManager.getSingleton().addOverlayElementFactory(fac);

            scnMgr.addRenderQueueListener(OverlaySystem.getSingleton());
        }

        public static void Start()
        {
            var app = new OverlayFactoriesExample();
            app.initApp();
            app.getRoot().startRendering();
            app.closeApp();
        }
    }
}
Could this be fixed by changing the org.ogre.OverlayElementFactory class to be abstract?

Best regards
paroj
OGRE Team Member
OGRE Team Member
Posts: 1993
Joined: Sun Mar 30, 2014 2:51 pm
x 1073
Contact:

Re: [C#] Should OverlayElementFactory be abstract?

Post by paroj »

the issue is that cross-language polymorphism is not enabled for OverlayElementFactory.
We need to enable it manually for all classes that need it. See http://www.swig.org/Doc3.0/SWIGDocument ... _directors

essentialy, a like like this is missing in OgreOverlay.i

Code: Select all

%feature("director") Ogre::OverlayElementFactory;
dunklemateria
Gnoblar
Posts: 7
Joined: Sun May 16, 2021 8:11 am

Re: [C#] Should OverlayElementFactory be abstract?

Post by dunklemateria »

Hello paroj,

thank you, I saw this in swig documentation, too. I tried your suggested modifications and registering is now possible. So good so far. Creating an OverlayElement using factory method is also possible. But it is not possible to cast it to its origin type:

Code: Select all

using System;
using org.ogre;

namespace OgreTest
{
    public class KeyListener : InputListener
    {
        ApplicationContext ctx;

        public KeyListener(ApplicationContext ctx)
        {
            this.ctx = ctx;
        }

        public override bool keyPressed(KeyboardEvent evt)
        {
            if (evt.keysym.sym == 27)
                ctx.getRoot().queueEndRendering();
            return true;
        }
    }

    public class CustomOverlayElement : PanelOverlayElement
    {
        public CustomOverlayElement(string instanceName) : base(instanceName)
        {
        }
    }

    public class CustomOverlayFactory : OverlayElementFactory
    {
        public override OverlayElement createOverlayElement(string instanceName)
        {
            return new CustomOverlayElement(instanceName);
        }

        public override string getTypeName()
        {
            return "Custom";
        }
    }

    public class OverlayFactoriesExample : ApplicationContext
    {
        InputListener listener;

        public OverlayFactoriesExample()
        {
            listener = new KeyListener(this);
        }

        public override void setup()
        {
            base.setup();
            addInputListener(listener);

            var root = getRoot();
            var scnMgr = root.createSceneManager();

            var shadergen = ShaderGenerator.getSingleton();
            shadergen.addSceneManager(scnMgr); // must be done before we do anything with the scene

            scnMgr.setAmbientLight(new ColourValue(.1f, .1f, .1f));

            var light = scnMgr.createLight("MainLight");
            var lightnode = scnMgr.getRootSceneNode().createChildSceneNode();
            lightnode.setPosition(0f, 10f, 15f);
            lightnode.attachObject(light);

            var cam = scnMgr.createCamera("myCam");
            cam.setAutoAspectRatio(true);
            cam.setNearClipDistance(5);
            var camnode = scnMgr.getRootSceneNode().createChildSceneNode();
            camnode.attachObject(cam);

            var camman = new CameraMan(camnode);
            camman.setStyle(CameraStyle.CS_ORBIT);
            camman.setYawPitchDist(new Radian(0), new Radian(0.3f), 15f);
            addInputListener(camman);

            var vp = getRenderWindow().addViewport(cam);
            vp.setBackgroundColour(new ColourValue(.3f, .3f, .3f));

            var fac = new CustomOverlayFactory();
            GC.SuppressFinalize(fac);
            OverlayManager.getSingleton().addOverlayElementFactory(fac);

            scnMgr.addRenderQueueListener(OverlaySystem.getSingleton());

            CustomOverlayElement overlay = (CustomOverlayElement)OverlayManager.getSingleton().createOverlayElementFromFactory("Custom", "test1"); 
        }

        public static void Start()
        {
            var app = new OverlayFactoriesExample();
            app.initApp();
            app.getRoot().startRendering();
            app.closeApp();
        }
    }
}
Leads to

Code: Select all

"System.InvalidCastException" ist in System.Private.CoreLib.dll aufgetreten.
Unable to cast object of type 'org.ogre.OverlayElement' to type 'OgreTest.CustomOverlayElement'.
I assume, that there is a polymorphism issue on org.ogre.OverlayElement, too. Do you agree?
paroj
OGRE Team Member
OGRE Team Member
Posts: 1993
Joined: Sun Mar 30, 2014 2:51 pm
x 1073
Contact:

Re: [C#] Should OverlayElementFactory be abstract?

Post by paroj »

I dont know if cross-language casting is possible at all. At other occasions we added casting code on the C++ side.
https://github.com/OGRECave/ogre/blob/d ... gre.i#L686

But there nothing about your C# code is known..

Maybe you can just implement & use the StringInterface API:
https://ogrecave.github.io/ogre/api/lat ... rface.html
dunklemateria
Gnoblar
Posts: 7
Joined: Sun May 16, 2021 8:11 am

Re: [C#] Should OverlayElementFactory be abstract?

Post by dunklemateria »

Yeah, I see. I fiddled arround, did some inspections and came across, that origin type of object is lost by creating a new object instance in wrapper methods:

Code: Select all

        public override OverlayElement createOverlayElement(string instanceName)
        {
            CustomOverlayElement ov = new CustomOverlayElement(instanceName);

            TypedReference tr = __makeref(ov);
            IntPtr ptr = default;
            unsafe
            {
                 ptr = **(IntPtr**)&tr;
            }

            return ov;
        }

Code: Select all

            var ov = OverlayManager.getSingleton().createOverlayElementFromFactory("Custom", "test1");
            TypedReference tr = __makeref(ov);
            IntPtr ptr = default;
            unsafe
            {
                ptr = **(IntPtr**)&tr;
            }
// in createOverlayElement() function
ptr 0x000001ba00019048 System.IntPtr

// At caller position
ptr 0x000001ba00019140 System.IntPtr


I will now search for different approach and take a look into your suggestion about the StringInterface. Thank you for your patience :)
dunklemateria
Gnoblar
Posts: 7
Joined: Sun May 16, 2021 8:11 am

Re: [C#] Should OverlayElementFactory be abstract?

Post by dunklemateria »

After some try-and-error and some basic knowlegdge about reflection I found a very-hacky-but-working approach, I want to share with you. Maybe someone take it and find a better solution.

First apply the director feature to OgreOverlayElement in OgreOverlay.i of Ogre source code (https://github.com/OGRECave/ogre/blob/d ... rlay.i#L64):

Code: Select all

%include "OgreOverlayElement.h"
#ifdef SWIGCSHARP
%feature("director") Ogre::OverlayElementFactory;
#endif
%include "OgreOverlayElementFactory.h"
Second create a custom overlay element, which suits your needs. It may derive from concrete overlay element class (such as org.ogre.PanelOverlayElement).

Code: Select all

    public class CustomOverlayElement : PanelOverlayElement
    {
        // Not really needed
        private string stringer;

        public string Stringer { get { return stringer; } }

        public CustomOverlayElement(string instanceName) : base(instanceName)
        {
        }
    }
Now it is possible to create a factory (for our custom overlay element) which derived from org.ogre.OverlayElementFactory, which could be registered using OverlayManager.createFactory:

Code: Select all

    public class CustomOverlayFactory : OverlayElementFactory
    {
        public override OverlayElement createOverlayElement(string instanceName)
        {
            CustomOverlayElement ov = new CustomOverlayElement(instanceName);
            ov.setCaption("Hello Overlay");
            return ov;
        }

        public override string getTypeName()
        {
            return "Custom";
        }
    }
The last key component is a helper class (credits for basic idea goes to Deadpikle https://github.com/swig/swig/issues/100 ... -310166874), which creates a new instance of our custom overlay instance and inject the swigCPtr and flag into the new instance as fastest solution without (un-/)marshalling or (de-/)serialization:

Code: Select all

    /// Helper class for swig specific operations
    public class SwigHelper
    {
        /// Create a down-casted element out of generic OverlayElement
        /// TODO: Add checks to T whether it is a down-castable class of OverlayElement
        public static T CastTo<T>(OverlayElement from)
        {
            // Retrieve method "getCPtr" via reflection
            System.Reflection.MethodInfo CPtrGetter = from.GetType().GetMethod("getCPtr", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
            // Store address in a proper variable
            HandleRef pa = ((HandleRef)CPtrGetter.Invoke(null, new object[] { from }));
            // Retrieve field which stores the memory owning flag
            System.Reflection.FieldInfo swigCMemOwnField = from.GetType().GetField("swigCMemOwn", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            // Read memory own flag into a local variable
            bool cMemoryOwn = (bool)swigCMemOwnField.GetValue(from);

            // Create a new instance of desired type with name of original instance
            T o = CPtrGetter == null ? default(T) : (T)System.Activator.CreateInstance
            (
                typeof(T),
                System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance,
                null,
                new object[] { from.getName() },
                null
            );
            // Retrieve the parent constructor method
            System.Reflection.ConstructorInfo constructor = typeof(OverlayElement).GetConstructor(
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
                null,
                new Type[] { typeof(IntPtr), typeof(bool) }, 
                null
            );
            // Execute parent constructor to inject swig native object address and memory owner flag
            constructor.Invoke(o, new object[] { pa.Handle, cMemoryOwn });

            return o;
        }
    }
This helper is used in combination with factory call:

Code: Select all

            // Create instance of factory
            CustomOverlayFactory fac = new CustomOverlayFactory();
            // Prevent from gc (thanks to paroj)
            GC.SuppressFinalize(fac);
            // Register factory
            OverlayManager.getSingleton().addOverlayElementFactory(fac);

            // Create a custom overlay element as desired type
            CustomOverlayElement cov = SwigHelper.CastTo<CustomOverlayElement>(OverlayManager.getSingleton().createOverlayElementFromFactory("Custom", "test1"));
To test this, apply some modification in CustomOverlayElement.createOverlayElement() method and debug using breakpoint into line where "cov" variable has been created. If all works well, your modification (here I added a custom caption) should be visible using a watch expression (e.g. "cov.getCaption()").

Thank you paroj for support and inspiration.

I would really like to see an StringInterface implementation against this, but I think it would take much more time to create an instance using parsing and reflection than simply injecting a pointer. But I know, this is very hacky and everybody need to know that. Use only in case you know, what happens.
Post Reply