2008-03-13

Improving Gadget Drawing

I received an email the other day from the chap who decided to write his own GUI, then decided to use Woopsi, then decided to switch back to his own GUI again. It seems he’s returning to Woopsi as it’s solved everyone’s least-favourite GUI problem - clipping.

Anyhoo, one of the questions he had was about the drawing system. At present, Woopsi’s gadget drawing system has a flaw. If you need to move a gadget, for example, and then resize the same gadget, you’d do something like this:


myGadget->moveTo(10, 10);
myGadget->resize(20, 50);

Makes sense. The problem is that both of these functions call the gadget’s draw() method. Drawing is one of the most expensive parts of the GUI’s functionality, so it’s not something it should really do unnecessarily. Also, it’s not something that the programmer should ever have to worry about. I really don’t want to start issuing draw() commands every time I need a gadget to update.

I came up with the following solution, but I haven’t really thought it through yet. It may have some problems. It will definitely introduce one new limitation - all drawing to a gadget will have to be done in the draw(clipRect) function, or in a function called by draw(clipRect).

The idea goes like this:

  • Add a new flag to the Gadget class called “requiresDraw” and a getter/setter pair.
  • Remove the draw() call from all functions and replace it with “setRequiresDraw()”.
  • Create a new gadget pointer array in the Woopsi class that stores pointers to gadgets that need redrawing.
  • Make the setRequiresDraw() function add the current gadget into the new Woopsi array if the gadget’s requiresDraw flag is false, then set the flag to true.
  • After all of the normal gadget processing has taken place, make the Woopsi class iterate through the new gadget pointer array calling the draw() method of each object and resetting its requiresDraw flag to false.
  • Modify the draw() method so that it raises a new GADGET_EVENT_DRAW event.

This has several advantages over the current system:

  • Hacky methods of avoiding superfluous drawing (like the MultiLineTextBox’s “setAutoDrawing()” function) can be removed.
  • Woopsi should run faster as less drawing operations will take place.
  • Developers can add their own drawing code by handling the new draw event.

I’d need to think some more about how the erase() method will work before doing this. Probably work a bit like this:

  • Call erase() - check if a new “erased” flag is false.
  • If erased is false, erase and set to true.
  • If erased is true, end function.

The erase() function has to be called immediately, because it’s called before a gadget moves or resizes. There’s no point in calling it if the gadget has already moved, as erase() would wipe the wrong area of the display.

The other option for improving drawing is to just add a list of dirty rectangles to the Woopsi class and get it to redraw those. This would mean recursing through the gadget tree until the dirty list is empty (similar to the current rectangle splitting code), but this would probably be slower than the existing solution or the one outlined above. However, it would probably mean I could remove the rect caches from the gadgets.

Comments

Jeff on 2008-03-13 at 07:56 said:

I don’t understand why the Woopsi class needs to be involved.

Just have the Gadget note itself that it is now deferring draws, maintain its own list of affected rectangles, and when you call the “setRequiresDraw(TRUE)” you process them yourself. You could even get away with just remembering the total affected rectangle.

I wonder whether this is because I think of Gadgets as controls, and not sprites - I doubt highly that I’d be writing applications that move controls around in a manner that exposes the area behind them (hmmm, scroll bar grips?). If I were writing a sprite based program, I’d be asking you for a full-on copy-masked-bitmap and I’d be working hard at managing dirty rectangles. But “user interface controls” are a different domain of problem - I mean, the arm9 is plenty fast enough to repaint a screen when you look at your demo and dragging windows around.

If its a ‘it looks unpleasant because it moves in two operations’, that I can sort of accept, but you could almost solve that by putting an opaque control (one that draws nothing) in front of the dirty area, do the move, then remove the opaque control to reveal the change.

ant on 2008-03-13 at 10:33 said:

Using the Woopsi gadget as the manager of all drawing operations means that drawing can be deferred until after all gadgets have completed their operations. Woopsi controls all of that in its “play()” function already; it just mean adding a new “processDrawQueue()” function after everything else has completed. It makes drawing operations transactional per frame.

If we say that each gadget has to manage its own deferred drawing, we’ll make the API more complicated - people will have to remember to disable and re-enable drawing before calling functions that trigger a redraw, which means that they will:

  • Need to prefix and suffix their code with method calls that disable, then re-enable and perform drawing operations;
  • Need to know which methods in each gadget cause draw operations.

It makes things more complicated and offers nothing over the behind-the-scenes queue version.

The reason for suggesting the change is that the guy using Woopsi is attempting to create an interface that will automatically resize itself as new gadgets are added. Say he’s got 10 gadgets on-screen and adds another, causing the display to rethink itself and resize/move all gadgets. That gives a total of 22 draw operations where only 11 are really needed.

Jeff on 2008-03-13 at 20:11 said:

Hmmm, seems to me that he just needs to make his SCREEN invisible, rearrange it, then make it visible again and it would work seamlessly.

Now, if he wants to do the fancy “controls all morph themselves around in slow animated crawls”, thats different. And way harder.

And again, I would point out that most people aren’t going to be moving controls around a lot, its really a FORM engine, not a SPRITE engine. Slowing down everyone so that one guy doesn’t need to put a few extra calls around his move/resize calls is not a sensible option.

Jeff on 2008-03-13 at 20:18 said:

Your mention of Woopsi::play() reminded me of a few more change requests.

The “woopsi.h” represents the Application object, as far as I’m concerned, though some might think of it as WindowManager. In any case, in every single one of my apps, I’m going to be doing this:


class WoopsiApplication : public Woopsi
{
public:

        // object construction
        inline WoopsiApplication()
        {
                initWoopsiGfxMode();
        }

        virtual inline ~WoopsiApplication()
        {
        }

        // called once at application startup
        virtual inline void Startup(void) {}

        // main application loop - runs forever, or until somethinng
        // makes it stop
        virtual inline void RunLoop(void) {
                // Infinite loop to keep the program running
                while (1) {
                        ProcessOneVBL();
                }
        }

        // single event loop - allows modal screens to pump messages without
        // giving up complete control
        virtual inline void ProcessOneVBL(void) {
                this->play();
                woopsiWaitVBL();
        }

        // called once at application shutdown
        virtual inline void Shutdown(void)
        {
        }
};

so that in my main apps, I can do:


int main(int argc, char *argv[])
{
        // Create woopsi application
        HP11C theApp;
        theApp.Startup();               // start it up
        theApp.draw();                  // ensure physical screen is up to date
        theApp.RunLoop();               // let the event loop run
        theApp.Shutdown();              // and we are done

        return 0;
}

The RunLoop() and ProcessOneVBL() are there to ensure that play() gets called in a consistent way, even when I’m running a modal window (like my file selector)


char *FileSelector::select_existing(
        const char *prompt )
{
        FileSelector *fs;
        fs = new FileSelector();
        if (fs) {
                fs->RunLoop();
                delete fs;
        }
        return NULL;
}

// pump the message loop - handleClick() will reset _looping if/when a button is
// pushed...
void FileSelector::RunLoop()
{
        _looping = true;
        while (_looping) {
                woopsiApplication->ProcessOneVBL();
        }
}

In my opinion, it would be valuable to push those things back into the top-level Woopsi class because every (well-structured) application is going to need them in some form - they’re inline so they cost little if they aren’t called.

Jeff on 2008-03-13 at 20:21 said:

On the previous comment, I know you like it that n00bs can just #include “woopsi.h” and it will work, but that slows things down enormously because it makes most source files dependent on every single header in the library.

Perhaps you could take the Woopsi class, merge my changes in, then move it to “woopsi_application.h” or some such, then #include that into “woopsi.h”

That way, those of us who know how to optimise our builds can just include the things we want while n00bs can still include just one file.

For me you could even change the name of the Woopsi class since I only use the singleton to access it :) - but that might mean lots of edits to the rest of the library.

Jeff on 2008-03-14 at 02:40 said:

Um, did my requested changes disappear into the spam filter? I’m sure I saw them???