Improvements to QGraphicsItem::CacheMode’s insides
I'm working on cleanups in Graphics View's cache mode / offscreen rendering support. Did the first part together with Alexis (who's on Tokamak these days btw!). Then I spent lots of time trying to get everything as simple and clean as it can be, in preparation for a few changes that will probably be useful for most QGV hackers. We started with a few clean-up goals:
- Removing as many string operations as possible from QPixmapCache handling
- Making a proper struct for storing all info associated with the off-screen buffer
- Ensuring that we use as little pixmap memory as necessary
- Removing rendering glitches / chopped off corners and so on
- Replacing today's bounding-rect-only support for partial updates of the cache with proper QRegion support
- Clipping to the exposed area to speed up partial cache updates
Then we had some higher goals, what we'd like to achieve with the cache in the future. Some examples are:
- Optimal performance with software and hardware rendering, especially on embedded hardware
- Removing "out of memory" bugs when you zoom too far in on DeviceCoordinateCache items
- Adding sub-surface caching to allow items and their children to be cached into the same surface
- Using an intermediate buffer for QGraphicsItem::opacity to avoid source-over glitches even in NoCache mode
- Adding support for graphics effects that make use of the off-screen buffer
Well, the struct is in place, the stringops are gone, we added region support, and we made sure we don't use more pixmapcache entries than strictly necessary. All the rendering glitches that have been reported (e.g., for flat items, chopped-off corners etc) are also gone. So today, I spent my Creative Friday experimenting with the "out of memory" bug in DeviceCoordinateCache and ended up with something pretty cool :-).
DeviceCoordinateCache stores the item in an offscreen buffer with the same as the item's bounds in device coordinates (pixels on screen). The item is painted once only for each time you transform the item or the view, and when you move it around it's just blitted from its buffer. KDE people can see this in action in plasmoids - moving them around is fast, and the plasmoids are not rerendered.
The problem is when the item is large, or you zoom in, this buffer typically gets pretty big. Zoom in more, and your app is likely to crash. Not so good. Try "print preview" in Assistant or the Text Edit demo and zoom in 8x to see what I mean (yes! it's embarrassing!). The excessive memory used to store the whole item seems even more silly when the only part that's drawn is quite small, and the rest is just wasted.
The solution I researched today is to clip this buffer to the viewport. In the image below, a large ellipse item is cached in DeviceCoordinateCache mode, but only the segment that's visible in the viewport is actually cached.
By clipping, we effectively cap the amount of memory this buffer can use. So no matter how far you zoom in, the same max pixmap size is used. Now, when either the viewport scrolls, or if the item moves, it's easy to just regenerate the cache by rerendering the whole item. But it's even more tempting to do "smart stuff", i.e., create a new pixmap, copy over the old content, and rerender the exposed areas tightly clipped, just like QWidget::scroll does for when you scroll a widget (blit+update instead of complete rerender). Something like this:
As the item moves by (dx, dy), the last (clipped) DeviceCoordinateCache buffer is in effect "shifted" by (dx, dy), and then copied into a replacement buffer which is (dx, dy) larger/smaller.
What's cool about this approach is that it gives you almost the same effectiveness as QWidget::scroll, but it works with both scrolling the viewport, or moving an item, or even translating it, regardless of its current transformation. And as opposed to QWidget::scroll, this approach allows semitransparent surfaces to be "scrolled". In fact the next step is to hook this up with QGraphicsItem::scroll, so that when you scroll an item that's cached, you're only scrolling its cache buffer (as opposed to today, where, ok I think this is pretty cool!, the scroll propagates all the way up to QGraphicsView::scroll, even for proxied widgets / try embedding a QWebPage and scroll it! try rotating 90 degrees and scroll again, it also works). I can't wait to see the performance of embedded QWebPage scrolling :-).
With these changes, it's easy to make a scrollarea in QGraphicsView that has several semitransparent layers (something Michael, a Brisbane troll, and I have discussed a while back), and when sliding the "content" layer, this is done by scrolling the offscreen buffer and reexposing. It's also bringing us closer to integrating composition effects like dropshadows, bloom and colorize, pixelate or even mesh-grid-transformations. Next week will be fun :-).
For those interested, I made a patch that applies the the 4.5.0 RC which shows progress so far (wish I could just share my branch, in the future I hope I can!). Click to download patch.
PS: A sideeffect of this patch is that DeviceCoordinateCache items, which earlier were only rendered completely or not at all, are now rendered "partially" when new areas are exposed. The print preview mode in Assistant is actually slower than usual because of this, yet it gives you the same performance regardless of the scale. Obviously this isn't good enough (it's caused by slowness in QPicture, which performs the same no matter how much/little is rendered at a time) so it requires some more work.
PPS: If you really don't want your items to be rendered "partially" like this, but rather keep the same behavior as today, well, I don't have any answer to that. Make it an option? Or only apply this feature to "large" items? Ideas are welcome.
Subscribe to our newsletter
Try Qt 6.6 Now!
Download the latest release here: www.qt.io/download.
Qt 6.6. is a feature release with focus on improving UX capabilities including responsive UI technology and the Qt Graph module.
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.