Building a bridge (to Qt)

Published January 24, 2017
Advertisement

Having made a good start on [font='courier new']Gx[/font] and having set up a game project that links to the [font='courier new']Gx[/font] headers and lib (in-place in the [font='courier new']Gx[/font] project for ease of update), I realised that a new game will need a new level editor, which for me means using the [font='courier new']Gx[/font] library from within a Qt project.

This has the most impact on the [font='courier new']Gx::GraphicsDevice[/font], which has now undergone some changes to support.

In a game project, [font='courier new']Gx[/font] just creates one graphics window. However, when using [font='courier new']Gx::GrahicsDevice[/font] in a Qt project for a level editor, we commonly want to be able to create any number of graphics views, on the fly, that can be hosted inside widgets.

So in addition to Gx, I have now made a start on a small library called [font='courier new']QGx[/font], which is designed to be a bridge between [font='courier new']Gx[/font] and Qt.

[font='courier new']QGx[/font] currently just contains [font='courier new']QGx::GraphicsDevice[/font], [font='courier new']QGx::GraphicsWidget[/font] and [font='courier new']QGx::Graphics[/font].

In order to facilitate this, [font='courier new']Gx::GraphicsDevice[/font] has been split inside the [font='courier new']Gx[/font] library into [font='courier new']Gx::AbstractGraphicsDevice[/font] and [font='courier new']Gx::GraphicsDevice[/font]. This is not for use in a polymorphic sense - an application will never use [font='courier new']Gx::AbstractGraphicsDevice[/font]. It is just a way to have the [font='courier new']Gx::GraphicsDevice[/font] and [font='courier new']QGx::GraphicsDevice[/font] share most of the methods.

So this is the public interface to [font='courier new']Gx::AbstractGraphicsDevice:[/font]

class AbstractGraphicsDevice{public: AbstractGraphicsDevice(); virtual ~AbstractGraphicsDevice(); void clear(Color color, float z); void setVertexDeclaration(const VertexDeclaration &declaration); void setVertexDeclaration(); void setVertexShader(VertexShader &shader); void setVertexShader(); void setPixelShader(PixelShader &shader); void setPixelShader(); VertexShader &vertexShader(); PixelShader &pixelShader(); void setTexture(Index stage, const Texture &texture); void setTexture(Index stage, const CubeMap &texture); void setTexture(Index stage); void renderTriangleList(const VertexBuffer &buffer); void renderLineList(const VertexBuffer &buffer); void renderTriangleList(const VertexBuffer &buffer, const IndexBuffer &indices); bool isLost() const; bool isReadyToReset() const;};Extended thus by [font='courier new']Gx::GraphicsDevice:[/font]

class GraphicsDevice : public AbstractGraphicsDevice{public: GraphicsDevice(); ~GraphicsDevice(); bool acquire(HWND hw, const DisplaySettings &settings); bool reset(const DisplaySettings &settings); bool reset(); void release(); void begin(); void end(); DisplaySettings settings() const;};This gives us the same [font='courier new']Gx::GraphicsDevice[/font] as we had before.

But now, in [font='courier new']QGx[/font] library, the public interface to [font='courier new']QGx::GraphicsDevice[/font] is as follows:

class GraphicsDevice : public Gx::AbstractGraphicsDevice{public: GraphicsDevice(); ~GraphicsDevice(); bool initialise(); void registerWidget(GraphicsWidget *widget); void unregisterWidget(GraphicsWidget *widget); bool reset(); void begin(GraphicsWidget *widget); void end(GraphicsWidget *widget); bool needsResetting() const; bool isReadyToReset() const; void scheduleReset();};A completely different approach, where you initialise once, then you add one or more [font='courier new']QGx::GraphicsWidget[/font] instances, and you also now specify which [font='courier new']QGx::GraphicsWidget[/font] you are targetting when you [font='courier new']begin()[/font] and [font='courier new']end()[/font].

There is some slight hackery in [font='courier new']QGx::GraphicsDevice[/font] that means we create a dummy device when there are no [font='courier new']QGx::GraphicsWidgets[/font] registered, inside the initialise call. This dummy device is destroyed and removed as soon as we add a [font='courier new']QGx::GraphicsWidget[/font], but it means we have a valid device before we create any [font='courier new']QGx::GraphicsWidgets[/font], so we are thus able to create global resources before we create the widgets. Not ideal, but most of the [font='courier new']Gx::GraphicsResources[/font] map to Direct3D types that need to be passed a valid device when they are created so I can live with it.

[font='courier new']QGx::GraphicsWidget[/font] is designed to be a base class that you derive your graphics views from. You then just implement the [font='courier new']paintEvent()[/font] method on your derived class, and use the accessor [font='courier new']QGx::Graphics &graphics()[/font] to access the [font='courier new']QGx::Graphics[/font] instance, which is an aggregate of [font='courier new']QGx::GraphicsDevice[/font] and a [font='courier new']Gx::GraphicsResources[/font] object (which is a typedef for [font='courier new']Gx::ResourceMap[/font]), all inherited from the [font='courier new']Gx[/font] library.

So you can now use all of the other [font='courier new']Gx::GraphicsResource[/font] classes in the Qt project and any new ones added are available automatically on a rebuild. It is only the set-up and view management methods that are different.

[font='courier new']QGx::GraphicsWidget[/font] provides the following interface:

class Graphics;class GraphicsWidget : public QWidget{ Q_OBJECTpublic: GraphicsWidget(Graphics &graphics, QWidget *parent = 0); ~GraphicsWidget();protected: virtual QPaintEngine* paintEngine() const; virtual void resizeEvent(QResizeEvent *event); Graphics &graphics();private: friend class GraphicsDevice; Graphics *g;};Internally, it implements the basics of registering and unresigstering itself from the device in the way you would expect:

QGx::GraphicsWidget::GraphicsWidget(Graphics &graphics, QWidget *parent) : QWidget(parent), g(&graphics){ setAttribute(Qt::WA_PaintOnScreen); setAttribute(Qt::WA_NoSystemBackground); setMinimumSize(64, 64); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); g->device.registerWidget(this);}QGx::GraphicsWidget::~GraphicsWidget(){ if(g) g->device.unregisterWidget(this);}QPaintEngine* QGx::GraphicsWidget::paintEngine() const{ return 0;}void QGx::GraphicsWidget::resizeEvent(QResizeEvent *event){ QWidget::resizeEvent(event); if(g) g->device.scheduleReset();}QGx::Graphics &QGx::GraphicsWidget::graphics(){ return *g;}So at the moment I have a simple test Qt application that allows you to add and remove graphics views from a [font='courier new']QVBoxLayout[/font] when buttons are clicked. [font='courier new']MyWidget[/font] is an example derived from [font='courier new']QGx::GraphicsWidget[/font], like so:

class MyWidget : public QGx::GraphicsWidget{public: MyWidget(QGx::Graphics &graphics, Gx::Color color, QWidget *parent);protected: virtual void paintEvent(QPaintEvent *event);private: Gx::Color color;};MyWidget::MyWidget(QGx::Graphics &graphics, Gx::Color color, QWidget *parent) : QGx::GraphicsWidget(graphics, parent), color(color){}void MyWidget::paintEvent(QPaintEvent *event){ if(graphics().device.isLost()) return; graphics().device.begin(this); graphics().device.clear(color, 1.0f); graphics().device.end(this);}So just like any other Qt widget really. In the [font='courier new']paintEvent()[/font], we first check to ensure the device is valid and, if so, we use the protected [font='courier new']graphics()[/font] method to get hold of the device and the resources map and use all the methods we inherit from [font='courier new']Gx::GraphicsDevice[/font] to do whatever rendering we like. The rendering is now constrained to this particular graphics view, whose size is taken from the widget.

If the widget is resized, this triggers a [font='courier new']reset()[/font] internally.

The main loop of the program fires a timer over and over, which calls a method on [font='courier new']MainWindow[/font]. This method calls back a method on [font='courier new']QGx::Graphics[/font] which checks for device lost and so on:

bool QGx::Graphics::testForReset(){ if(device.isLost()) { if(!device.isReadyToReset()) { return false; } } if(device.isReadyToReset()) { device.scheduleReset(); } if(device.needsResetting()) { for(auto &r: resources) { if(r.isDeviceBound()) { r.release(); } } if(!device.reset()) { return false; } for(auto &r: resources) { if(r.isDeviceBound()) { r.reset(device); } } deviceReset(*this); } return true;}The [font='courier new']MainWindow[/font] timer method then emits a [font='courier new']render()[/font] signal, which we connect to the [font='courier new']update()[/font] slot on any [font='courier new']QGx::GraphicsWidget[/font] classes we create.

void MainWindow::renderTimeout(){ if(!graphics.testForReset()) { return; } emit render();}So when we hit the button to create a new view in [font='courier new']MainWindow[/font], it is just standard [font='courier new']QWidget[/font] stuff now, except we have to pass the [font='courier new']QGx::Graphics[/font] to the widget's constructor.

void MainWindow::buttonPressed(){ static Gx::Color colors[3] = { Gx::Color(1, 0, 0, 1), Gx::Color(0, 1, 0, 1), Gx::Color(0, 0, 1, 1) }; static int index = 0; MyWidget *widget = new MyWidget(graphics, colors[index++], this); if(index >= 3) index = 0; connect(this, SIGNAL(render()), widget, SLOT(update())); centralWidget()->layout()->addWidget(widget);}And when we hit the button to delete the last view in the list:

void MainWindow::removePressed(){ QLayoutItem *i = centralWidget()->layout()->takeAt(centralWidget()->layout()->count() - 1); delete i->widget(); delete i;}That automatically takes care of unregistering the widget from the [font='courier new']QGx::GraphicsDevice[/font]'s internal list.

So all ready to go when I want a GUI application that uses Direct3D windows now. Thankfully my old level editor is working well enough to not have to worry about that quite yet.

I've added the [font='courier new']GxPhysics[/font] module to Gx now as well. It isn't fully formed yet, but it is essentially a [font='courier new']Gx::Physics[/font] object representing the world, a [font='courier new']Gx::Body[/font] object that wraps up Bullet's [font='courier new']btRigidBody[/font] and a whole bunch of shapes deriving from [font='courier new']Gx::Shape[/font] which wrap individual Bullet shape classes such as [font='courier new']Gx::Polyhedron[/font] for [font='courier new']btConvexHull[/font] and [font='courier new']Gx::Sphere[/font] for [font='courier new']btSphere[/font] and so on.

I've started a bare-bones game project now that can load physics objects and graphic meshes from my old level editor and display the level and a debug render of the physics polyhedra. As I'm working now, I'm adding things to [font='courier new']Gx[/font] as and when I need them which is proving a reasonable work-flow - lots of jumping between two projects and lots of clean/rebuild going on, but at least things feel a bit tidier and setting up new projects in the future will be a lot easier.

Hopefully soon I'll achieve something pretty enough to post some screenshots instead of all this code. We'll see :)

Thanks for stopping by.

Previous Entry Framework Fun
3 likes 5 comments

Comments

ongamex92

I kind of wonder if rendering to a renderTarget/frameBuffer and then displaying the image in a Qt window would be better, sure you'll kind of display each image twice but it shouldn't be that slower.

January 25, 2017 10:55 AM
Dirk Gregorius

Nice article, some screenshots would be nice. Also a link to your editor if publicly available.

Did you see that Qt is breaking its connection to OpenGL to allow implementations based on DirectX or Vulkan. I think the major refactor happened in the just released 5.8. Here is a blog post:

http://blog.qt.io/blog/2016/01/28/qt-and-direct3d-12-first-encounter/

January 25, 2017 11:56 PM
Aardvajk

I kind of wonder if rendering to a renderTarget/frameBuffer and then displaying the image in a Qt window would be better, sure you'll kind of display each image twice but it shouldn't be that slower.


You could, although I'm not sure what the advantage over just rendering directly to the window would be? Qt exposes the HWND for a widget so its all just like Win32 rendering really.

Nice article, some screenshots would be nice. Also a link to your editor if publicly available.

Did you see that Qt is breaking its connection to OpenGL to allow implementations based on DirectX or Vulkan. I think the major refactor happened in the just released 5.8. Here is a blog post:
http://blog.qt.io/blog/2016/01/28/qt-and-direct3d-12-first-encounter/


Thanks. No, I hadn't read that. Will have a Google.
January 29, 2017 02:13 PM
ongamex92

You could, although I'm not sure what the advantage over just rendering directly to the window would be? Qt exposes the HWND for a widget so its all just like Win32 rendering really. 

Well if you have an OpenGL implementation you will have to deal with multiple context(as there is 1 context per window) and sharing resource between these context(note that not all resource are shareable). For D3D this isn't a problem as all resource are manged by a single *device* and are by default shared between context and can be used with multiple swapchains.
Hmm, for OpenGL it is possible to use only 1 context to render the window images and then share that image and display it to the target window.

To be honest I haven't implemented that myself so I'm pretty curious how you will implement it.

January 30, 2017 07:43 AM
Aardvajk
[quote name="imoogiBG" timestamp="1485762209"]You could, although I'm not sure what the advantage over just rendering directly to the window would be? Qt exposes the HWND for a widget so its all just like Win32 rendering really. Well if you have an OpenGL implementation you will have to deal with multiple context(as there is 1 context per window) and sharing resource between these context(note that not all resource are shareable). For D3D this isn't a problem as all resource are manged by a single *device* and are by default shared between context and can be used with multiple swapchains. Hmm, for OpenGL it is possible to use only 1 context to render the window images and then share that image and display it to the target window. To be honest I haven't implemented that myself so I'm pretty curious how you will implement it. [/quote] I never do any OpenGL stuff. If I had to, there are existing Qt libraries I believe. Might end up doing some 3D stuff at work. We recently upgraded our software to Qt 5 there which had some new 3D stuff based on OpenGL I think.
February 02, 2017 02:56 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement