[ cross-posted to Java2D-Interest ]


Jason Boyd wrote:
>
> > Create a Graphics2D instance on the BufferedImage and go...  and
> > when you're done swap the component's current BufferedImage w/the
> > just rendered image and repeat.  There are, of course, corner
> > cases and synchronization issues that need to be dealt with.
>
> Such as? Would it be enough to synchronize on the BufferedImage
> whenever accessing it? I am unfamiliar enough with Swing's
> double-buffering that I'm not sure in what cases I'd be creating a
> deadlock.
>

See _end of message_ for some code snippets of how to render
offscreen in a general way.

I have a "layer"-like framework that was influenced by Vincent
Hardy's GLF work (see his book and Knudsen's) and I needed a way
to render asynchronously so I wrote some code that seems to handle
this quite nicely.  I like my approach more the other idioms I've
seen.  Realize that this is aimed at doing asynchronous rendering
and *not* animation.

> > I guess I don't understand how your data model maps into J2D's
> > user space.  Why is a slow lookup required to map into J2D's user
> > space?
>
> My component displays multi-tracked, time-based data. That is, it
> renders 1 or more (typically a dozen or so) *tracks*, where each
> track contains a "list" of timed data. Each datum has a start and
> end time, so the final display renders tracks vertically and time
> horizontally.

OK, I see.  It's like a gantt chart or time&activity bar chart.

> So in other words, even if I'm only repainting a narrow strip of the
> component, I have to figure out for each visible track what the
> first timed element is that would be visible at that slice (even if
> there is no such element) and then iterate up to the last timed
> element rendering each. This is *almost* fast enough to handle
> repaints for every frame in a 30 fps situation, but not quite (and
> I'm on a fast machine). Since I know it *should* be possible to
> render the moving playhead without repainting the tracks (and
> therefore without any access of the data model) I am going to go
> this route, as it is clearly the wiser algorithm.

So render the entire chart into a BufferedImage.  Once.  And only
update if the component is resized or made incompatible w/the
BufferedImage.  Then in the paint() method blit the image into the
current Graphics instance.  Then composite the playhead onto the image
using an appropriate compositing rule.  This doesn't require
repainting the track image since it's pre-rendered.


====================================================

Some code snippets that might help you get started:

First, create a class that handles _all_ the rendering.

  --------------------------------------------------------------
  ...

  public FooRenderer()
  {
     ...
  }

  public void runLater()
  {
    // up to you to figure out how to run the "run"
    // method below...  there might be synchronization
    // issues, etc.  if your coordination requirements
    // are trivial then this method and run() could
    // simply use the wait() and notify() primitives
    // to kick off a rendering.
  }

  public void run()
  {
    // synchronized fetch of buffered image
    BufferedImage bi = component.getRenderingBuffer();
    // render into image
    if (bi != null)
      {
        Graphics2D g2d = bi.createGraphics();

        // *** render here ***
        // foo.render(g2d,bi.getWidth(),bi.getHeight());

        g2d.dispose();

        component.setRenderedBuffer();
      }
  }
  ...
  --------------------------------------------------------------

Next, subclass JComponent (or some sort of Component, I
assume you're using Swing) and implement extra methods
that deal with BufferedImage management.

  --------------------------------------------------------------
  ...

  // buffer references used for rendering
  private transient BufferedImage newBuffer;
  private transient BufferedImage renderingBuffer;
  private transient BufferedImage renderedBuffer;
  private transient BufferedImage paintBuffer;

  // if there is no buffer available for rendering this flag indicates
  // that the renderer should be run ASAP
  private transient boolean rendererRequestsNotification;

  ...

  void setRenderer(FooRenderer renderer)
  {
    this.renderer = renderer;

    // prod the Component in order to set up BufferedImage
    repaint();
  }

  ...
  --------------------------------------------------------------

Now it gets hairy...

  --------------------------------------------------------------

  /**
   * Paints this component.  An offscreen buffer is created on the
   * first paint request and the composition is requested only once to
   * paint into the buffer.
   *
   * @see java.awt.Component#paint
   */
  public void paint(Graphics g)
  {
    // if there isn't a need to render, then do nothing
    // this needs to be controlled at a higher
    // level -- add pause/resumePainting() methods and control them
    // through a WindowListener interface instance.

    Dimension     size = getSize();
    BufferedImage bi   = getPaintBuffer(size);

    if (bi != null)
      {
        // blit the image into the Graphics context
        g.drawImage(bi,0,0,null);
        toolkit.sync();
      }
    else
      {
        // a resize must have recently happened so there is no image
        // available for rendering, so... just paint the background
        // and continue.
        g.setColor(getBackground());
        g.fillRect(0,0,size.width,size.height);
        toolkit.sync();
      }
  }

  --------------------------------------------------------------

Add the methods that "step" BufferedImages through various
states...

  --------------------------------------------------------------

  private BufferedImage createBuffer(Dimension size)
  {
    return new BufferedImage(size.width,size.height,
                             BufferedImage.TYPE_INT_ARGB);
  }

  /**
   * Return true if buffer and size aren't equal.
   * Requires buffer to be non-null.
   */
  private boolean wrongSizedBuffer(BufferedImage buffer, Dimension size)
  {
    return (buffer.getWidth() != size.width) ||
      (buffer.getHeight() != size.height);
  }

  synchronized BufferedImage getRenderingBuffer()
  {
    if (newBuffer != null)
      {
        renderingBuffer = newBuffer;
        newBuffer       = null;
        return renderingBuffer;
      }
    else
      {
        rendererRequestsNotification = true;
        return null;
      }
  }

  synchronized void setRenderedBuffer()
  {
    renderedBuffer  = renderingBuffer;
    renderingBuffer = null;

    repaint();
  }

  synchronized BufferedImage getPaintBuffer(Dimension size)
  {
    boolean notifyRenderer = false;

    // a rendered image is available
    if (renderedBuffer != null)
      {
        if (wrongSizedBuffer(renderedBuffer,size))
          {
            paintBuffer    = null;
            renderedBuffer = null;
            notifyRenderer = true;
          }
        else
          {
            newBuffer      = paintBuffer;
            paintBuffer    = renderedBuffer;
            renderedBuffer = null;
            notifyRenderer = rendererRequestsNotification;
            rendererRequestsNotification = false; // reset
          }
      }
    // only the current paint buffer is available
    else if ((paintBuffer != null) && (wrongSizedBuffer(paintBuffer,size)))
      {
        paintBuffer    = null;
        notifyRenderer = true;
      }

    // 1) (eventually) ensure there are always two buffers available
    //    -- one for painting and one for rendering.  the speed at
    //    which the JFC EventQueue invokes this component's paint()
    //    method 'clocks' the swapping of render and paint buffers
    //
    // 2) optimization -- if newBuffer is the wrong size, proactively
    //    replace it instead of waiting for it to cycle through the
    //    rendering process

    int bufferCount = 0;
    if (newBuffer != null)       ++bufferCount;
    if (renderingBuffer != null) ++bufferCount;
    if (renderedBuffer != null)  ++bufferCount;
    if (paintBuffer != null)     ++bufferCount;

    if (newBuffer == null)
      {
        if (bufferCount < 2)
          newBuffer = createBuffer(size);

        if (bufferCount < 1)
          notifyRenderer = true; // notify renderer
      }
    else
      {
        if (wrongSizedBuffer(newBuffer,size))
          newBuffer = createBuffer(size);
        // no need to notify renderer here
      }

    // notify renderer
    if ((renderer != null) && notifyRenderer)
      renderer.runLater();

    return paintBuffer;
  }
  --------------------------------------------------------------


That's it.  This logic should keep Swing from ever slogging while
waiting for a render to finish.  The only oddness is that if you
resize and rendering is slow you might see a blank component until
a BufferedImage is rendered.  It also uses, in steady state, two
BufferedImages so it can be memory intensive.  You could optimize
this to create BufferedImages of less than screen depth (a good
idea) if you know something about your application.  And many
other optimizations are possible... :)

Oh, I won't know if this runs on OS-X until maybe next week but
it should, right? ;)

Hope this helps,


-ASM

--
Allan MacKinnon
[EMAIL PROTECTED]
Boston, MA

===========================================================================
To unsubscribe, send email to [EMAIL PROTECTED] and include in the body
of the message "signoff JAVA2D-INTEREST".  For general help, send email to
[EMAIL PROTECTED] and include in the body of the message "help".

Reply via email to