2007-11-30

Woopsi Progress!

Made a bit of progress with Woopsi (source in SVN, as usual). First of all, the Window class no longer includes all those +/- 1 values when handling border sizes. Next, I’ve removed any calls to Gadget::getType() by refactoring the need for it out. Much tidier.

There are a couple of places where gadgets make unchecked assumptions about their parent’s/children’s type and cast to that type. The ScreenFlipGadget assumes that its parent is a screen, for example, and Woopsi assumes that its children are also screens. However, this isn’t too much of a problem, as Woopsi should only ever contain classes that are (or inherit from) screens, and the ScreenFlipGadget should only ever be a child of a screen.

I’ve added the decoration code, too. The Gadget class now includes a “decoration” flag in its Flags struct. If this is set to “true”, the gadget is automatically inserted into a reserved section at the start of the parent gadget’s child vector when it is added to that gadget. When depth swapping its children, a gadget ignores anything in the decorations section of the vector.

The reserved decorations section isn’t really partitioned off from the rest of the vector; it’s implemented by remembering how many decoration gadgets there are in the vector. Non-decoration gadgets are always added to the vector either at the end or at _gadgets[_gadgets.begin() + _decorationCount]). Decoration gadgets are always added at same location or at the start of the vector, at which point the _decorationCount value is increased by one.

This has two main benefits. I no longer need to worry about swapping depths with something that shouldn’t be depth-swapped because it’s a part of the containing gadget. Secondly, I didn’t need to implement a separate _decorations vector, which would have added complications all over the place.

The SuperBitmap class now contains an “isDecoration” parameter in its constructor, which controls whether it is added as a standard SuperBitmap gadget or as a decoration (in which case it is borderless and treated as a window or screen background image).

So, nothing particularly exciting, but it tidies up some of the tedious stuff that needed to be sorted out first.

EDIT:

Oh, whilst I’m on, some thoughts about the GraphicsPort object:

Argh.

Implementing something like this will actually be rather difficult. It’s easy for OSX and Windows because of the way I assume they work with their windows - the same way the Amiga did. They buffer their windows in memory and draw them to the screen as needed, like the SuperBitmap. Woopsi doesn’t do this - it draws everything from scratch as and when it is necessary. Drawing to the SuperBitmap is easy. It works like this:

  • Constructor called
    • Reserve a region of RAM
  • Draw a line to the reserved RAM
  • Main draw function called
    • Calculate visible portions of SuperBitmap
    • Copy visible portions of reserved RAM to screen

If we try to do the same thing without buffering the drawing somewhere else first, we get this for every primitive drawing function:

  • Draw function called
    • Calculate visible portions of GraphicsPort
    • Draw visible portions of primitive to screen

A rectangle is built from 4 lines. That means we’d need to re-calculate the visible portions of the Graphics port 4 times per rectangle. As calculating a rectangle’s visible regions is the most computationally-expensive algorithm in Woopsi, this is incredibly inefficient. The only way around is would be to have a “prepareToDraw()” function that caches the visible regions of the GraphicsPort class before any other drawing functions are called. Either that, or each primitive drawing command checks whether a variable (call it “_preparedToDraw”) is true before drawing, and if not runs the “prepareToDraw()” function before drawing anything. The variable and cache would need to be flushed out either every VBL (dumb solution) or any time anything on the screen shifts around and invalidates its cache (smart solution, but horribly difficult to implement). Raises an interesting question - should Woopsi’s gadgets maintain a cache of their visible portions, and only recalculate when they have been invalidated by something higher up the z-order moving/resizing/closing? I think that’s a more Windows-style solution. More memory usage, though.

I realised this a while ago (here, but it looks like I didn’t explain the reasons), which is why I decided to rename all of the drawing functions within the gadgets to drawIntXXX(). The internal drawing functions within each gadget shouldn’t be used without a valid clipping rect as they will otherwise draw outside the boundaries of the containing gadget, and generating this clipping rect is too expensive to do for every draw command. Drawing using these commands should only be done within the “draw(Rect clipRect)” function, as that is automatically passed the correct clipping rect for each visible portion of the gadget.

EDIT EDIT:

Caching visible portions of gadgets makes so much sense that I can’t ignore it. It’ll need to work like this:

  • Gadget moves/resizes/closes
  • Calculates regions that have become visible/overlapped
  • Sends those newly invalidated regions to all siblings below it in the z-order
  • Siblings remove overlapped regions from their cache and add newly visible regions (I’ve got to add rectangle merging here or I’ll end up with massive fragmentation problems). As each gadget adds the overlapped regions to its cache they get removed from the list being sent through the vector, and the function ends when the vector is empty (same as the current drawing system)

That should make the drawing routines even faster than they already are; it’ll also mean that the GraphicsPort class can be implemented incredibly easily.

Now, is there anything I’ve overlooked? Will screen dragging break the caching system unless I’ve stored the rectangles relative to the gadget? Does this imply lots of screen->gadget space conversion? Hmmm…

Another thought - what if I just send an “invalidated” command to any siblings within the regions invalidated by a higher gadget moving/etc, which empties those gadgets’ cache? When they attempt to redraw, they rebuild their cache using the existing code. No need for huge changes and complicated new functionality. Not as quick to run, but so much easier to code!

Comments

Jeff on 2007-12-01 at 01:12 said:

I’m not sure why you are concerned about the GraphicsPort but perhaps its because you’re thinking they have a longer life than I do.

I would think that they get created on the fly as/when they are needed (for example, when the list gadget wants its datasource to draw a row. You don’t need to recompute valid rectangles on every graphic function since the state/size/etc of the graphicsport will not change during the draw operations. Sure, if it was stupid enough to create the graphics port, draw the first row, let some other stuff move around, draw the second row, etc you’d have problems. But thats not rational behaviour.

As to needing offscreen storage, MacOS never really did double-buffering especially back when they had 128K of RAM to play with. Whilst offscreen gworlds came along later, the machine really could do its graphics ops directly to video ram.

Given the expected behaviour of the list Gadget, it might be sensible to have special “GraphicsPort::moveToScreen(x,y)” which adjusted its mapping into video ram while maintaining size, thus leaving offscreen memory intact. And I don’t think you need to be able to resize a graphics port since that would be as memory/computationally expensive as deallocating one and creating another.

ie, I’d allocate a GraphicsPort that was the size of one ROW in the list, and shuffle it up and down, if it was going to use backing memory.

One trick that the Mac definitely did use was to adjust the origin of the plane being drawn into. Thus, although the port USER drew to logical coordinates (0,0), that might be translated to different physical coordinates. If your graphics vram is based at (px,py) you record that base address, AND a delta to add to logical to get physical. Thus, the caller does port->setPixel(lx,ly) which is translated into screen->setPixel(lx+dx,ly+dy) which then sets the hardware pixel. To shift the port down the screen, you just adjust the (dx,dy) values appropriately. The physical address occupied by the port remains the same, but logically drawing to (0,0) now goes somewhere else in the pixel plane.

The physical clipping region didn’t change even though the logical origin of the port did.

Deferring updates via an “invalidated” message is definitely a sensible way to go - and I would think that its unlikely, in normal apps, that GADGETS are going to move around all that much - its far more likely that WINDOWS are going to move. Perhaps there’s space for an optimisation there as well; I mean, OSX Cocoa mandates “no overlapping views” presumably for performance reasons - there’s no reason why you can’t say “overlapping GADGETS will result in graphic anomalies” (with the exception that you’d like a background gadget to work correctly)

Jeff on 2007-12-01 at 01:17 said:

And, of course, its important to note that the GraphicsPort I was envisaging was primarily to make Gadgets easier when drawing (such as the List Gadget) - there’s no reason that all gadgets have to be pushed to using GraphicsPorts.

The real problem to solve is “how do you make it easiest for a List Gadget (or subclass thereof) to display custom data”.

In my day job, this is the biggest issue we face - losing sight of the original problem and wandering off in search of the coolest technological solution; you end up designing an atomic bomb to crack an egg.

Jeff on 2007-12-01 at 01:21 said:

While thinking about ‘moving’ gadgets, don’t forget that you can hide them these days as well, cant you?

Do you update a gadgets clipping info if its hidden? [no] Or do you update it when its made visible again? [yes]