8

Imagine a huge rectangular grid filled with tiles. The individual tiles are not very complicated, they are svg images containing a low amount of shapes.

The number of different types of tiles in not very large, I estimate in the low hundreds. However, the grid can become very large, so the number of total tiles is huge (at least tens of thousands, maybe more).

I have to be able to smoothly scroll the grid both horizontally and vertically, as well as smoothly zoom it in and out. I also have to be able to jump to a specific position.

It would also be nice if I could populate it asynchronously, first the elements which are actually visible, and then the rest. This means that a table-handling class where I first have to add rows and columns in a loop would not be the best solution, because the starting position is not necessarily the upper left corner.

Zooming is simply achieved by having all the width and height properties of the items within a tile specified as a multiple of a scaling factor. The svg shouldn't be a problem as the number of different images is not high, it should be able to be cached. In the unlikely case svg became the bottleneck, I could just use sets of different pngs in different resolutions.

I tried (or considered) the following approaches:

  1. Using the methods of the SameGame example, creating QML objects dynamically (Component.createObject). This works if the number of objects is small, but is very slow with a large number of objects. Even if the objects are completely empty, this method takes a very long time.

  2. Using a Repeater inside a Flickable. The Flickable contains a Grid, which is populated by a Repater. The Grid, of course, is immense. This method is faster than creating the objects dynamically, but still inefficient as the number of tiles grows. The QML engine keeps track of every item, even those which are not visible. Zooming is also quite slow, as the properties of every item are recalculated, not just the visible ones.

  3. Using a GridView. This looks like the perfect solution at a first glance. The GridView inherits Flickable, and it also takes care to only render contents which are within the bounds of the view. Even a test case with millions of svg images runs reasonably fast, and it scrolls and resizes smoothly. There is only one problem: The GridView is only flickable either horizontally or vertically, but not both. There has been a feature request about this since 2012, but it still seems to be ignored.

  4. Using a QGraphicsView directly. It is capable of displaying, scrolling and zooming the needed amount of elements, but it's not QML-based. The rest of my GUI is in QML, and I've only read horror stories about combining QML and QGraphicsView. I've never seen any reasonable examples of it.

What other solutions are there? Some horrible hack of using Javascript to add and remove rows and columns of a simple GridLayout (which is only a couple rows and columns larger than the visible area) while it is moved around in a Flickable? Or just embedding an OpenGL window and drawing everything manually?

I hope this shouldn't be an impossible task. There were strategy games written more than 20 years ago for DOS and Windows 95 which could handle this amount of tiles, while additionally having textures and animations.

Community
  • 1
  • 1
vsz
  • 4,811
  • 7
  • 41
  • 78
  • 1
    Comparisons against DOS and Windows 95 do not make any sense, as the underlying technologies and approaches are extremely different. Why not using a custom QtQuick element that does what you need? – peppe Aug 31 '16 at 11:24
  • @peppe : my point was not the system itself, but that it was possible to do it with 200-times less computational power. And by "custom QtQuick element" you mean I should implement my own 2d graphics engine from scratch? Yes, it would be possible, but that approach is similar to the OpenGL drawing solution. The `GridView` had the advantage of knowing what and when to load and unload. By doing everything from scratch, I have to implement my own memory management, visibility calculations, everything. Then why use Qt if I have to implement everything from scratch? – vsz Aug 31 '16 at 11:37
  • 1
    You totally can even today, but a Repeater with 10000 elements is just the wrong approach. The fact that QML is lacking such a built-in thing doesn't mean it's impossible to achieve. – peppe Aug 31 '16 at 11:39
  • The visibility calculation should not be that hard to achieve, when you know the size of the area to display and the size of the tiles. Then use a Repeater with Loader as delegates, and set a binding for the "active"-property matching the visibility constraints (depending on the index). – derM - not here for BOT dreams Aug 31 '16 at 12:10
  • I've made a similar thing with a custom QtQuick element like that: an invisible square grid with a tiles of all types is made in QML and passed to my custom QtQuick element. Then this texture is used as an atlas for rendering the tiles with C++. (there is a code available somewhere) Tiles are still composable with QML - what QML does well. You'll have to do something similar if you don't want a QObject per tile. – Velkan Sep 01 '16 at 06:38
  • @Velkan : you mean something like using a `Canvas` and `Context2D` to avoid having separate objects for the individual images? – vsz Sep 02 '16 at 07:10
  • Using Canvas/Context2D avoids having separate objects, but for me it's cumbersome because it's js. And it'll probably be slow, because it's composed with js in the GUI thread, and then somehow synced by Qt to the render thread. – Velkan Sep 02 '16 at 07:24
  • @Velkan : in that case what do you recommend? The "chip example" runs perfectly fine on my target system, and it has 40000 objects. It uses `QGraphicsView`, however. I would like to use my application on a relatively low-end system, where a `Repeater` of even 1000 or 2000 QML objects causes a significant FPS drop, even if the objects only contain a single `Rectangle` each. The 40000 object `QGraphicsView` runs very smoothly, even though every object has several shapes and texts. – vsz Sep 02 '16 at 07:43
  • I don't have recommended solutions. I'm just saying that the hardest approach has worked for me: with a custom QtQuick element inherited from `QQuickItem` in C++ with OpenGL rendering, using things like `QSGGeometryNode`, `QSGGeometry` and `QSGSimpleMaterialShader`. – Velkan Sep 02 '16 at 08:01
  • @Velkan : I still have no idea how to embed it into QML though. I'm studying it at the moment and there don't seem to be elegant solutions: http://stackoverflow.com/questions/13014415/qt5-embed-qwidget-object-in-qml and for Qt Quick 2 there seems to be no solution at all. If I was able to embed QObjects into QML, I would have already used a `QGraphicsView` and my problem would have been solved. (Edit: OK, my mistake, I see you derive from `QQuickItem` instead of from `QObject`) – vsz Sep 02 '16 at 08:10
  • I think your path of least resistance is to get the source code to GridView (available here: http://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/items/qquickgridview.cpp) and fix QTBUG-26582 yourself. – David K. Hess Sep 02 '16 at 12:08
  • 1
    Check out https://github.com/bjorn/tiled sources maybe you will find something usefull there – j2ko Sep 05 '16 at 16:05

1 Answers1

2

Yeah, Qt is very good at ignoring community suggestions for years, even if they would be extremely useful, considered important, and happen to be the most up-voted, such as zip support.

I personally wouldn't bother "fixing" GridView, but rather implement something from scratch that suits my specific requirements, in C++ so that it is fast and efficient. And it will be very easy if your tiles are uniform squares, and it sounds like you could get away with that, even if the actual images inside are not square. This will make it very easy to determined their positions programmatically, and also determine the top left corner tile, how much tiles per line and the stride for the subsequent lines. Then as the visibility rectangle moves you iterate your container and signal to create QML elements for those which enter visibility. Easy peasy.

You don't need anything fancy, just inherit QObject, register the type to QML, then go and populate it's internal "model". You definitely do not want to have all the objects in memory, even if the scene graph is smart enough to not render them, it will still be processing them, I suspect your drop in FPS is not the product of a GPU but a CPU bottleneck.

The actual grid object can emit creation and destruction signals with their data Q_SIGNAL void create(x, y, imgPath);, so you bind custom handlers on the QML side, which will give you flexibility and ease of use, such as easily specifying the "delegate" object, it will be more elegant than doing the actual creation/destruction in C++. You can use bindings on the QML side for the few items that are visible to track when they go out of screen to self-destruct, that would minimize complexity, as you won't have to track all the "living" objects.

Component {
   id: objComponent
   Image {
      property bool isVisible: { is in grid.visibleRect ??? }
      onIsVisibleChanged: if (!isVisible) destroy()
   }
}

MyGrid {
  id: grid
  contentX: flickable.contentX
  contentY: flickable.contentY

  onCreate: objComponent.createObject(flickable.contentItem, {"x" : x, "y" : y, "source" : imgPath})
}

Flickable {
   id: flickable
   contentWidth: grid.contentWidth
   contentHeight: grid.contentHeight
}

Normally, when a user has a question, important enough to offer a bounty I'd produce working code, but unfortunately I am currently too busy. The concept is pretty simple though and should not be too problematic to implement.

dtech
  • 47,916
  • 17
  • 112
  • 190