GRIN Animation Framework

optimized drawing

Introduction and Scope

GRIN Animation Framework is a highly efficient animation framework designed around the needs of Blu-ray Java and other TV devices that use PBP. It provides support for doing double-buffered animation with optimized drawing, so that a minimum of pixels are erased and copied with each frame.

Animation Framework Overview

GRIN includes an animation framework that manages displaying frames of animation on a regular basis.It supports different drawing modes, like repaint draw and direct draw. It allows you to define your own code-based animations by implementing an AnimationClient interface.These custom animations can be painted above or below GRIN's Show-based painting. Of course, you can also define new GRIN features that do their own painting, and make them part of a Show. Here's a UML diagram illustrating the animation framework:

animator classes

The menu xlet in com.hdcookbook.bookmenu.menu uses the GRIN animation framework. The Gun Bunny game xlet in com.hdcookbook.gunbunny doesn't use GRIN, but it has the same kind of frame pump. Gun Bunny even lets you switch dynamically between direct draw, repaint draw and SFAA. With all three, it maintains time-based animation, dropping frames if it falls behind.

To use the animation framework, you'll need a bit of boilerplate code that looks like this:

    import com.hdcookbook.grin.animator.AnimationClient; 
    import com.hdcookbook.grin.animator.AnimationEngine;
    import com.hdcookbook.grin.animator.AnimationContext;
    import com.hdcookbook.grin.animator.DirectDrawEngine;
    import com.hdcookbook.grin.animator.RepaintDrawEngine;
    import com.hdcookbook.grin.Show;

    ...

    public class MyXlet implements Xlet, Runnable, AnimationContext {

    private XletContext context;
    private AnimationEngine engine;
    private Show show;

    ...

    public void initXlet(XletContext ctx) throws XletStateChangeException {
        this.context = ctx;
        DirectDrawEngine dde = new DirectDrawEngine();
        // or repaint draw, ...
        dde.setFps(24000);
        engine = dde;
        engine.initialize(this);
    }

    public void startXlet() throws XletStateChangeException {
        engine.start();
    }

    public void pauseXlet() {
        engine.pause();
    }

    public void destroyXlet(boolean unconditional) throws XletStateChangeException {
        engine.destroy(); // Doesn't return until thread is stopped

        ... destroy HScene ...
    }

    // This is called from the animation manager as the xletInit
    // action. It's run in the animation frame, so it's OK to
    // do time-consuming things, l
    public void animationInitialize() throws InterruptedException {
        ...

        show = ... a new Show instance ...;
        ... initialize show ...;
        engine.checkDestroy();

        engine.initNumTargets(... the number you need ...);
        AnimationClient[] clients = ... your clients (Show is a client) ...;
        engine.initClients(clients);
        Rectangle bounds = ... 1920x1080, or a smaller bounds ...;
        Container c = ... the container, probably just the HScene ...;
        engine.initContainer(c, bounds);
    }

    public void animationFinishInitialization() throws InterruptedException {
        show.activateSegment(show.getSegment("S:Initialize"));
        // Or whatever the initial segment's name is
    }

This creates an Animator worker thread that will display the show, by default at 23.976 times per second.

The animation loop within the AnimationEngine happens within a single animation thread, created by the engine. In pseudocode, the flow of the animtion loop is:

    context.animationInitialize(); // Call xlet's initialization code
    for each AnimationClient c
        c.initialize()
    context.animationFinishInitialization(); // sets UI state for first frame
    for (frame = 0 to infinity) {
        wait until it's time for the next frame
        for each AnimationClient c
            Advance c's model to next frame
        If the animation isn't behind
            for each AnimationClient c
               Tell c we're caught up
               Ask c where it plans to draw
            for each AnimationClient c
                for each rectangle r that needs to be re-drawn
                    erase drawing buffer as necessary
                    set a clipping rectangle
                    Ask c to paint the current frame to drawing buffer
            for each rectangle needs to be redrawn
                copy from drawing buffer to the screen
    }

Optimized Drawing

To achieve good drawing performance, it's essential to minimize drawing. This usually means avoiding the re-drawing of pixels that are the same as they were in the last displayed frame of animation. This can be difficult to figure out manually, so the animatiom framework contains support to help automate this text. This is centered around the class DrawRecord.

An instance of the class DrawRecord represents a bit of drawing in a rectangular area of the screen. Within that rectangle, your drawing can be fully opaque, or have some pixels that are transparent, or that aren't drawn to at all. The drawing you do can be the same as it was in the last frame, or it can be different. If you're writing your own Java code, you can do what the GRIN show graph nodes do: Represent each logical piece of drawing with a DrawRecord. If there's some drawing that was done in the previous frame that isn't present in the current frame, DrawRecord keeps track of this for you, too: It remembers what was drawn in the last frame, and if any of those DrawRecord instances aren't included in the set of what is drawn in the current frame, those areas of the screen are automatically erased and re-drawn. In the GRIN scene graph, every visual feature maintains its own DrawRecord instance.

The best way to visualize what's going on is to see it for yourself, live. The GrinView program has a function to do this. Run the program grin/scripts/shell/run_grinviewer.sh test, and double-click S:Initialize. You'll see a silly little animation, with turtles and rabbits flying around the screen. Click on "Stop", then on "Watch drawing," then on "+frame". Now, just click on "next" a bunch of times. For each frame, it will show you the areas of the screen buffer that are erased in red, then the areas that are drawn in green, then it will show you the result.

By default, this GRIN show selects a segment that makes pretty good use of drawing optimization (S:DrawOpt.Opt.TargetAndGuarantee). But to start, double-click on S:DrawOpt.Unoptimized. You'll notice that a large area of the screen is updated with each frame: the bounding rectangle of screen objects that change, in fact.

The framework could treat each DrawRecord as an independent drawing operation, and try to merge only some of them together when necessary. Unfortunately, this is a hard computational problem to solve - the most straightforward algorithm has O(n3) time complexity, which means the amount of time it takes is proportional to the cube of the number of inputs. If you assume a complex scene broken down into 50 drawing operations, each represented by a DrawRecord, then the execution time becomes proportional 503, which is 125,000 - a pretty big number.

There are "heuristic algorithms" that can attempt to do this more quickly, but they take CPU power, too. For the GRIN aniamtion framework, we take a different approach. Realizing that a BD-J disc has an author, we assume that the author can spend some time optimizing drawing. It's not too complicated: The author just needs to specify which drawing operations belong together, that is, which sets of DrawRecord instances should be grouped into the same bounding rectangle. These groupings are called "draw targets." For a simple animation there might be only one draw target, but if a few things are going on at once, it might make sense to have up to four or five draw targets.

This is what is done with the segment called S:DrawOpt.Opt.Target. Double-click on that, and step through the animation a few times. You'll see that the turtles are in one draw target (the default target for the show, T:Default), the bunny spaceship is in a second target (T:Bunny), and the fading picture of the bunny with the shotgun in a third (T:Picture). This results in much more efficient drawing.

Generally, you'll want to put objects that are close to each other in the same draw target. It's OK if objects in different draw targets overlap - you'll notice that the objects in this sample move all over the place, and sometimes cover each other. No matter how you assign draw targets, the results will always display correctly, but if you assign them well, the screen will be repainted faster.

In the sample GRIN script, there's a further level of optimization applied in the S:DrawOpt.Opt.TargetAndGuarantee segment. Here, a samll additional optimization is made: A GRIN feature is added to tell the animation framework that the two turtle troopers completely fill the rectangle they're drawn in with opaque pixels, so there's no need to erase that part of the graphics buffer. Correctly applying this can result in a small speedup. Note that there's a slight wrinkle: The two turtle troopers are actually in seperate .png image files, and there's a gap between the two. In order to guarantee that a contiguous rectangle is completely filled, the "guarantee_fill" feature actually fills in an opaque black rectangle between the two images.

Summary of AnimationEngine control flow

The control flow of the animation engine is summarized in the following table:

AnimationEngine decides... Method called in AnimationClient (e.g. Show) Method called within the AnimationClient (e.g. the Show's features)
1) It's time to advance the logical model to the next frame #nextFrame() Each feature on the screen updates its internal data model to the state it should be in for the next frame
2) It's not behind in the animation loop, so the next frame can be drawn to the screen
2.a) Tell each client it's about to be displayed #setCaughtUp()
2.b) Find out where each AnimationClient plans to draw #addDisplayAreas(RenderingContext) Each visual feature calls RenderContext.addArea(DrawRecord) to record this frame's drawing operations
2.c) Compute the optimized set of "damage rectangles" that need to be re-drawn to guantee a visually correct result
2.d) For each damage rectangle, ask the clients to draw into the screen buffer #paintFrame(Graphics2D) Each visual feature paints itself, as clipped by the Graphics2D's clip rect
2.e) For each damage rectangle, blt the screen buffer out to the screen.

Conclusion

This animation framework is really quite general and flexible. We hope it will form the basis of efficiently combining drawing down by scene graph frameworks like the GRIN scene graph, as well as drawing done directly in code.

Fork me on GitHub