Documentation Archive Developer
Search

ADC Home > Reference Library > Technical Notes > Legacy Documents > User Experience >

Legacy Documentclose button

Important: This document is part of the Legacy section of the ADC Reference Library. This information should not be used for new development.

Current information on this Reference Library topic can be found here:

Principia Off-Screen Graphics Environments

CONTENTS

Using Color QuickDraw to draw off screen is a common requirement of applications and other kinds of programs that run on the Macintosh. This Note discusses what Color QuickDraw needs in a graphics environment and how to create one for off-screen drawing. A brief discussion of GWorlds, which are off-screen graphics environments that are set up by the system, is given to help you decide whether to use them or the do-it-yourself techniques described in this Note for setting up an off-screen graphics environment. The author's intent is to provide concepts and routines for creating an off-screen graphics environment, and also to explain why existing routines for off-screen drawing act as they do.

[Jul 24 2000]






Introduction

Many, many thanks go to Guillermo Ortiz, Konstantin Othmer, Bruce Leak, and Jon Zap for all their expertise on this subject, Rich Collyer, Rick Blair, and Jim Friedlander for paving the way, and especially to all people who inspired this update by asking great off-screen drawing questions.

Off-Screening

The Macintosh, as with every other CPU ever made by Apple, has memory-mapped video. That is, what you see on the screen is just the visual representation of a part of memory that's reserved for the video hardware (that's stretching the truth just a bit in the case of the text screens of the original Apple computer, the Apple II line, and the Apple III because there's also a character generator in those, but the overall process still looks roughly the same). If you change the contents of a memory location in this part of memory, then you'll see the corresponding location on the screen change when the video hardware draws the next frame or field of video. The resident raster graphics package, QuickDraw in the case of the Macintosh, draws images by stuffing the right values into the right places in the part of memory reserved for the video display. The resulting image on the screen looks like a line or perhaps an oval if you asked QuickDraw to draw a line or an oval, or it could be an entire complex image if you asked QuickDraw to draw one. This is normal, on-screen drawing.

Because video memory is a part of RAM just like any other part of RAM in the memory map of the Macintosh (or almost like; video memory might exist on a NuBus(TM) video card, but it's still RAM), QuickDraw can be told to draw into a part of memory that isn't reserved for the video hardware, maybe into a part of your own application's heap. When you tell QuickDraw to draw into a part of memory that's not reserved for the video hardware, you can't see any of the results. This is off-screen drawing. There are plenty of perfectly good reasons to do this, such as providing storage for a paint-style document or to smoothly animate an image, but the assumption here is that you have a perfectly good reason to do this so you're more interested in the "how" of it instead of the "why" of it. If you need to know why, there are several books that cover off-screen drawing and the perfectly good reasons to do such a thing. A good place to start is Scott Knaster's book, Macintosh Programming Secrets, referenced at the end of this Note.

This Note is divided into these major sections:

  • The introduction is the part that you're reading now.
  • The Building Blocks provides an overview of the data structures that you need to tell Color QuickDraw to draw off screen.
  • Building the Blocks discusses the construction and initialization of these data structures.
  • Playing With Blocks shows an example of the use of these structures to draw off screen.
  • Put That Checkbook Away! discusses some variations of these techniques to handle off-screen drawing for special cases.
  • The GWorld Factor provides a brief overview of GWorlds, how to use them, and how they compare and contrast to the manual techniques that are described in most of this Note.

Those of you who aren't quite sure whether to use GWorlds or the do-it-yourself techniques might want to skip ahead for a moment to "The GWorld Factor" just in case doing it yourself is a waste of time. In any case, it's a good idea to read this whole Note because the concepts are mostly the same whether you're using GWorlds or not. GWorlds just make the process a lot easier, and they let you take advantage of the 8*24 GC video card. But, we're not in that section of the Note yet.

Back to top

The Building Blocks

Before you can tell QuickDraw to draw off of the screen, you'll need to build three major data structures: a CGrafPort, a PixMap, and a GDevice. You'll also need a couple of tables that define the colors involved with drawing to and copying from the off-screen image: the color table and the inverse table. Of course, you'll need the pixel image itself, which is often called the "pixel buffer" or the "image buffer" or the "off-screen buffer" or just "the buffer." It's always called the "pixel image" in this Note. It doesn't necessarily buffer anything anyway.

The CGrafPort

A CGrafPort describes a drawing environment, and it's the color version of the GrafPort structure that's described on pages 147 through 155 in the QuickDraw chapter of Inside Macintosh Volume I. The drawing environment consists of, among other things, the size and location of the graphics pen, the foreground and background colors to use when something is drawn, the pattern to use, the region to clip all drawing to, and the portion of a pixel image that the CGrafPort logically exists in. Any initialized CGrafPort or GrafPort can be set as the current port through the _SetPort routine. The current port is a set of parameters that are implicitly passed to most QuickDraw routines.

The most important reason to build a new CGrafPort when you draw off screen rather than using an existing CGrafPort is so that switching between drawing to an off-screen graphics environment and drawing to one or more windows (each of which is an extended GrafPort or CGrafPort structure) on the screen is very easy. Some people use just one CGrafPort to share between on-screen and off-screen graphics environments, and switch their PixMap structures to switch between drawing on screen and drawing off screen. That does work, but if the off-screen and on-screen graphics environments have a different clipRgn, visRgn, pen characteristic, portRect, or any other characteristics that are different, then those must be switched at that time too. If you instead create a CGrafPort that's dedicated to one graphics environment, then a simple call to _SetPort effectively switches all these things for you at once. That's why every window on the screen comes with its own port. A simple call to _SetPort switches between the characteristics of each window even if each window has radically different drawing characteristics.

The CGrafPort data structure is more completely described in the "Color QuickDraw" chapter of Inside Macintosh Volume V, pages 49 through 52, and in the "Graphics Overview" chapter of Inside Macintosh Volume VI, pages 16-12 through 16-13.

The PixMap

A pixel image alone is just a formless blob of memory. Pixel maps, defined by the PixMap structure, describe pixel images, giving them a form and structure that's suitable for Color QuickDraw to draw into them and copy from them. The PixMap structure tells you the dimensions and location in memory of the pixel image, its coordinate system, and the depth and format of the pixels. Pixel maps that describe indexed-color pixel images additionally describe the colors that are represented by the values of the pixels in the pixel image. This is done through the color table, also known as the color look-up table or CLUT. Color tables are attached to pixel maps through their pmTable field. Direct-color pixel images have pixel values that describe their own colors, and so color tables aren't needed for those.

The PixMap structure is described in the "Color QuickDraw" chapter of Inside Macintosh Volume V, pages 52 through 55, and in the "Graphics Overview" chapter of Inside Macintosh Volume VI, pages 16-11 through 16-12. The concept of direct-color and indexed-color pixels is described in this same chapter on pages 16-16 through 16-18, and also in the "Color QuickDraw" chapter of the same volume on pages 17-4 through 17-10.

The GDevice

Graphics devices, defined by the GDevice structure, describe color environments. They're the most misunderstood data structure when it comes to off-screen graphics environments for three major reasons: first, they're not originally documented as being relevant to humans; second, they look as though they're only for screens; and third, it looks as though color tables describe color environments. We can dispose of these myths here: graphics devices are documented as being useful to humanity in this Note at least; they're critically important for both on-screen and off-screen drawing; and color tables describe the colors in pixel images, not color environments.

What's all this about color environments? In theory, there are virtually three hundred trillion colors available with Color QuickDraw through the 48-bit RGBColor record. In reality, there are never this many colors available, and in fact there might be only two. Color QuickDraw maps the theoretical color that you specify to the pixel value of the closest available color in the current color environment. This can be done with a color table, but that's not very efficient. Finding the closest available color to an RGBColor in a color table means searching the entire color table for that one closest color. If that's done just once, then performance isn't much of an issue, but if it's done many times, the performance hit could be significant. A very bad case of this is _CopyBits, where every pixel value in the source image is converted to an RGBColor by looking it up in the color table of the source PixMap. If the color table of the destination PixMap had to be searched to find the closest available color for every pixel in the source PixMap, then the performance of even the most straightforward _CopyBits call could be a lot slower than it has to be.

To avoid this performance hit, the current GDevice provides an inverse table and a device type which are used to determine the available set of colors. Inverse tables are anticolor tables. Where color tables give you a color for a given pixel value, inverse tables give you a pixel value for a given color. Every conceivable color table has a corresponding conceivable inverse table, just as every positive real number has a corresponding negative real number, or every Mr. Spock has a corresponding Mr. Spock with a goatee. The device type specifies whether the color environment uses the indexed-color, fixed-color, or direct-color model. In the direct-color model, the inverse table is empty. Only the indexed-color and direct-color models are described in this Note.

When you specify a color in an indexed-color environment, Color QuickDraw takes the RGBColor specification and converts it into a value that can be used as an index into the inverse table of the current GDevice. To do this conversion, Color QuickDraw takes the top few significant bits of each color component and combines them into part of a 16-bit word, blue bits in the least significant bits, green bits right above it, and the red bits right above green bits. Any unused bits are in the most significant bits of the 16-bit word. The resulting 16-bit word is used as an index into the inverse table. The value in the inverse table at that index is the pixel value which best represents that color in the current color environment. The number of bits of each component that are used is determined by what's called the "resolution" of the inverse table. Almost always, the resolution of an inverse table is four bits, meaning the most significant four bits of each component are used to form the index into the inverse table. Figure 1 shows how an RGBColor record is converted to an index into an inverse table when the inverse-table resolution is four.

Figure 1

Figure 1. Conversion of RGBColor Record to Inverse-Table Index

The same process is used when _CopyBits is called with an indexed-color destination. Each pixel in the source pixel image is converted to an RGBColor either by doing a table look-up of the source pixel map's color table if the source pixel image uses indexed colors, or by expanding the pixel value to an RGBColor record if the source pixel image uses direct colors. The resulting RGBColor is then used to look up a pixel value in the inverse table of the current GDevice, and this pixel value is put into the destination pixel image.

If you specify a color in a direct-color environment, then the resulting RGBColor is converted to a direct pixel value by the processes that are shown on pages 17-6 through 17-9 of the "Color QuickDraw" chapter of Inside Macintosh Volume VI.

Usually, inverse-table look-up involves an extra step to find what are called "hidden colors" using proprietary information that's stored at the end of the inverse table. With an inverse-table resolution of four, only 16 shades of any particular component can be distinguished, and that's often not enough. An inverse table with a resolution of five is much larger, but it still only gives you 32 shades of any component. Hidden colors are looked up after the normal inverse-table look-up to give a much more accurate representation of the specified color in the current color environment than the inverse-table look-up alone can produce. Sometimes, most notably when the arithmetic transfer modes are used or if dithering is used, the hidden colors are ignored.

When a new color table is assigned to a PixMap or when its existing color table is modified, then a new corresponding inverse table should be generated for the GDevice that'll be used when drawing into that environment. Normally, this happens automatically without you having to do any more than inform Color QuickDraw of the change. This is described in more detail in "Changing the Off-Screen Color Table" later in this Note.

Graphics devices are documented in the "Graphics Devices" chapter of Inside Macintosh Volume VI which supersedes the "Graphics Devices" chapter of Inside Macintosh Volume V. They're also discussed in the "Graphics Overview" chapter of Inside Macintosh Volume VI, pages 16-13 through 16-14. The inverse-table mechanism is described in the "Color Manager" chapter of Inside Macintosh Volume V, pages 137 through 139.

All Together Now

There are a lot of different ways to put the three structures together, and this Note discusses the architecture that's shown in Figure 2. This architecture is useful when you want a simple, atomic, off-screen graphics environment.

Figure 2

Figure 2. Relationships Between Structures for Off-Screen Drawing

Notice that there's no way to get to the GDevice from the CGrafPort, nor is there a way to get to the CGrafPort from the GDevice, though the PixMap can be found through either one. Your application must keep track of both the CGrafPort and the GDevice.

Back to top

Building the Blocks

As with just about any algorithm, there are many ways to put the different structures together that form an off-screen graphics environment. This section covers just one way to build the architecture that's shown in Figure 2.

Building the CGrafPort

The CGrafPort structure is the easiest one to put together because the _OpenCPort routine initializes so many of the fields of the CGrafPort structure for you. It also allocates and initializes the structures that are attached to every CGrafPort, such as the visRgn, clipRgn, grafVars handle, and so forth. Most of these are initialized with values that are fine for general purposes, but the visRgn, clipRgn, and portRect fields should be set to the desired boundary rectangle of the off-screen graphics environment. What follows is an overview of each of the fields that you have to worry about when you're setting up a CGrafPort for drawing off screen.

portPixMap handle to the off-screen PixMap. _OpenCPort initializes this field to a copy of the PixMap that's attached to the gdPMap field of the current GDevice. An overview of setting up this PixMap for drawing off screen is given in "Building the PixMap" later in this Note.

portRect specifies the rectangular area of the associated pixel image that this CGrafPort controls. This field should be set to the desired rectangular area of the off-screen image because _OpenCPort doesn't necessarily initialize it to this size. Usually, the top-left corner of this rectangle has the coordinates (0, 0), but not necessarily so.

visRgn handle to the region that specifies the visible area into which you can draw. _OpenCPort doesn't necessarily initialize it to the size of the off-screen image, so it should be set to the same size and coordinates as the portRect and left at that. This field is more important for windows because parts of them can be hidden by other windows.

clipRgn handle to the region that specifies the logical area into which you can draw. _OpenCPort initializes it to cover the entire QuickDraw coordinate plane. It's usually a good idea to set it to the same size and coordinates as the portRect to avoid problems if the clipRgn is scaled or translated, which causes its signed integer coordinates to overflow and turn it into an empty region. One of the most common cases of this occurs when a picture that's created in this CGrafPort is drawn into a destination rectangle that's any larger than or translated from the original picture frame. Everything in the picture, including the clip region, is scaled to fit the destination rectangle. If the clip region covers the entire QuickDraw coordinate plane, then its coordinates overflow their signed integer bounds, and the clip region becomes logically empty. The result is that nothing is drawn.

The CreateOffScreen routine in Listing 1 creates an off-screen graphics environment, given a boundary rectangle, pixel depth, and color table, and it returns a new off-screen CGrafPort and GDevice, along with an error code. The desired pixel depth in bits per pixel is given in the depth parameter. If the pixel depth is eight or less, then an indexed-color graphics environment is created and a color table is required in the colors parameter. If the pixel depth is 16 or 32 bits per pixel and 32-Bit QuickDraw is available, then a direct-color graphics environment is created and the colors parameter is ignored. If 32-Bit QuickDraw isn't available, then a pixel depth of 16 or 32 bits per pixel results in CreateOffScreen doing nothing more than returning a parameter error. A description of CreateOffScreen is given following the listing.

MPW Pascal Listing 1

FUNCTION CreateOffScreen(
   bounds:         Rect;       {Bounding rectangle of off-screen}
   depth:          Integer;    {Desired number of bits per pixel in off-screen}
   colors:         CTabHandle; {Color table to assign to off-screen}
   VAR retPort:    CGrafPtr;   {Returns a pointer to the new CGrafPort}
   VAR retGDevice: GDHandle    {Returns a handle to the new GDevice}
   ): OSErr;

   CONST
      kMaxRowBytes = $3FFE; {Maximum number of bytes in a row of pixels}

   VAR
      newPort:     CGrafPtr;     {Pointer to the new off-screen CGrafPort}
      newPixMap:   PixMapHandle; {Handle to the new off-screen PixMap}
      newDevice:   GDHandle;     {Handle to the new off-screen GDevice}
      qdVersion:   LongInt;      {Version of QuickDraw currently in use}
      savedPort:   GrafPtr;      {Pointer to GrafPort used for save/restore}
      savedState:  SignedByte;   {Saved state of color table handle}
      bytesPerRow: Integer;      {Number of bytes per row in the PixMap}
      error:       OSErr;        {Returns error code}

BEGIN
   (* Initialize a few things before we begin *)
   newPort := NIL;
   newPixMap := NIL;
   newDevice := NIL;
   error := noErr;

   (* Save the color table's current state and make sure it isn't purgeable*)
   IF colors <> NIL THEN
      BEGIN
         savedState := HGetState(Handle(colors));
         HNoPurge(Handle(colors));
      END;

   (* Calculate the number of bytes per row in the off-screen PixMap *)
   bytesPerRow := ((depth * (bounds.right - bounds.left) + 31) DIV 32) * 4;

   (* Get the current QuickDraw version *)
   error := Gestalt(gestaltQuickdrawVersion, qdVersion);
   error := noErr;

   (* Make sure depth is indexed or depth is direct and 32-Bit QD installed*)
   IF (depth = 1) OR (depth = 2) OR (depth = 4) OR (depth = 8) OR
         (((depth = 16) OR (depth = 32)) AND (qdVersion >= gestalt32BitQD))
    THEN
      BEGIN
         (* Maximum number of bytes per row is 16,382; make sure within range*)
         IF bytesPerRow <= kMaxRowBytes THEN
            BEGIN
               (* Make sure a color table is provided if the depth is indexed*)
               IF depth <= 8 THEN
                  IF colors = NIL THEN
                     (* Indexed depth and clut is NIL; is parameter error *)
                     error := paramErr;
            END
         ELSE
            (* # of bytes per row is more than 16,382; is parameter error *)
            error := paramErr;
      END
   ELSE
      (* Pixel depth isn't valid; is parameter error *)
      error := paramErr;

   (* If sanity checks succeed, then allocate a new CGrafPort *)
   IF error = noErr THEN
      BEGIN
         newPort := CGrafPtr(NewPtr(SizeOf (CGrafPort)));
         IF newPort <> NIL THEN
            BEGIN
               (* Save the current port *)
               GetPort(savedPort);

               (* Initialize the new CGrafPort and make it the current port*)
               OpenCPort(newPort);

               (* Set portRect, visRgn, and clipRgn to the given bounds rect*)
               newPort^.portRect := bounds;
               RectRgn(newPort^.visRgn, bounds);
               ClipRect(bounds);

               (* Initialize the new PixMap for off-screen drawing *)
               error := SetUpPixMap(depth, bounds, colors, bytesPerRow,
                     newPort^.portPixMap);
               IF error = noErr THEN
                  BEGIN
                     (* Grab the initialized PixMap handle *)
                     newPixMap := newPort^.portPixMap;

                     (* Allocate and initialize a new GDevice *)
                     error := CreateGDevice(newPixMap, newDevice);
                  END;

               (* Restore the saved port *)
               SetPort(savedPort);
            END
         ELSE
            error := MemError;
      END;

   (* Restore the given state of the color table *)
   IF colors <> NIL THEN
      HSetState(Handle(colors), savedState);

   (* One Last Look Around The House Before We Go... *)
   IF error <> noErr THEN
      BEGIN
         (* Some error occurred; dispose of everything we allocated *)
         IF newPixMap <> NIL THEN
            BEGIN
               DisposCTable(newPixMap^^.pmTable);
               DisposPtr(newPixMap^^.baseAddr);
            END;
         IF newDevice <> NIL THEN
            BEGIN
               DisposHandle(Handle(newDevice^^.gdITable));
               DisposHandle(Handle(newDevice));
            END;
         IF newPort <> NIL THEN
            BEGIN
               CloseCPort(newPort);
               DisposPtr(Ptr(newPort));
            END;
      END
   ELSE
      BEGIN
         (* Everything's OK; return refs to off-screen CGrafPort and GDevice*)
         retPort := newPort;
         retGDevice := newDevice;
      END;
   CreateOffScreen := error;
END;

MPW C Listing 1

#define kMaxRowBytes 0x3FFE /* Maximum number of bytes in a row of pixels */

OSErr CreateOffScreen(
    Rect       *bounds,     /* Bounding rectangle of off-screen */
    short      depth,       /* Desired number of bits per pixel in off-screen*/
    CTabHandle colors,      /* Color table to assign to off-screen */
    CGrafPtr   *retPort,    /* Returns a pointer to the new CGrafPort */
    GDHandle   *retGDevice) /* Returns a handle to the new GDevice */
{
    CGrafPtr     newPort;     /* Pointer to the new off-screen CGrafPort */
    PixMapHandle newPixMap;   /* Handle to the new off-screen PixMap */
    GDHandle     newDevice;   /* Handle to the new off-screen GDevice */
    long         qdVersion;   /* Version of QuickDraw currently in use */
    GrafPtr      savedPort;   /* Pointer to GrafPort used for save/restore */
    SignedByte   savedState;  /* Saved state of color table handle */
    short        bytesPerRow; /* Number of bytes per row in the PixMap */
    OSErr        error;       /* Returns error code */

    /* Initialize a few things before we begin */
    newPort = nil;
    newPixMap = nil;
    newDevice = nil;
    error = noErr;

    /* Save the color table's current state and make sure it isn't purgeable*/
    if (colors != nil)
    {
        savedState = HGetState( (Handle)colors );
        HNoPurge( (Handle)colors );
    }

    /* Calculate the number of bytes per row in the off-screen PixMap */
    bytesPerRow = ((depth * (bounds->right - bounds->left) + 31)
    >>5) << 2;

    /* Get the current QuickDraw version */
    (void)Gestalt( gestaltQuickdrawVersion, &qdVersion );

    /* Make sure depth is indexed or depth is direct and 32-Bit QD installed*/
    if (depth == 1 || depth == 2 || depth == 4 || depth == 8 ||
            ((depth == 16 || depth == 32) && qdVersion >=gestalt32BitQD))
    {
        /* Maximum number of bytes per row is 16,382; make sure within range*/
        if (bytesPerRow <= kMaxRowBytes)
        {
            /* Make sure a color table is provided if the depth is indexed */
            if (depth <= 8)
                if (colors == nil)
                  /* Indexed depth and clut is NIL; is parameter error */
                  error = paramErr;
        }
        else
            /* # of bytes per row is more than 16,382; is parameter error */
            error = paramErr;
    }
    else
        /* Pixel depth isn't valid; is parameter error */
        error = paramErr;

    /* If sanity checks succeed, then allocate a new CGrafPort */
    if (error == noErr)
    {
        newPort = (CGrafPtr)NewPtr( sizeof (CGrafPort) );
        if (newPort != nil)
        {
            /* Save the current port */
            GetPort( &savedPort );

            /* Initialize the new CGrafPort and make it the current port */
            OpenCPort( newPort );

            /* Set portRect, visRgn, and clipRgn to the given bounds rect */
            newPort->portRect = *bounds;
            RectRgn( newPort->visRgn, bounds );
            ClipRect( bounds );

            /* Initialize the new PixMap for off-screen drawing */
            error = SetUpPixMap( depth, bounds, colors, bytesPerRow,
                    newPort->portPixMap );
            if (error == noErr)
            {
                /* Grab the initialized PixMap handle */
                newPixMap = newPort->portPixMap;

                /* Allocate and initialize a new GDevice */
                error = CreateGDevice( newPixMap, &newDevice );
            }

            /* Restore the saved port */
            SetPort( savedPort );
        }
        else
            error = MemError();
    }

    /* Restore the given state of the color table */
    if (colors != nil)
        HSetState( (Handle)colors, savedState );

    /* One Last Look Around The House Before We Go... */
    if (error != noErr)
    {
        /* Some error occurred; dispose of everything we allocated */
        if (newPixMap != nil)
        {
            DisposCTable( (**newPixMap).pmTable );
            DisposPtr( (**newPixMap).baseAddr );
        }
        if (newDevice != nil)
        {
            DisposHandle( (Handle)(**newDevice).gdITable );
            DisposHandle( (Handle)newDevice );
        }
        if (newPort != nil)
        {
            CloseCPort( newPort );
            DisposPtr( (Ptr)newPort );
        }
    }
    else
    {
        /* Everything's OK; return refs to off-screen CGrafPort and GDevice*/
        *retPort = newPort;
        *retGDevice = newDevice;
    }
    return error;
}

CreateOffScreen begins by making sure that the color table, if there is one, doesn't get purged during the time that the off-screen graphics environment is created. Then, a sanity check is done for the given depth, bounds, and color table. The depth must be either 1, 2, 4, or 8 bits per pixel, or additionally 16 or 32 bits per pixel if 32-Bit QuickDraw is available. If these conditions aren't satisfied, then it's decided that there's an error in the parameter list, and CreateOffScreen does nothing more. To determine whether 32-Bit QuickDraw is available or not, the _Gestalt routine is used. If _Gestalt returns a value that's equal to or greater than the constant gestalt32BitQD, then 32-Bit QuickDraw is available and depths of 16 and 32 bits per pixel are supported. It's not necessary to determine whether _Gestalt is available or not because it's implemented as glue code in the Macintosh Programmer's Workshop.

A check is then done to determine whether the number of bytes in each row of the off-screen pixel image is too much for QuickDraw to handle. Color QuickDraw can handle up to and including 16,382 ($3FFE) bytes in each row of any pixel image. If the required number of bytes per row exceeds this amount, then CreateOffScreen decides that there's an error in the parameter list and does nothing more. The minimum number of bytes in a row that's enough to cover the given boundary rectangle at the given pixel depth is calculated with the formula:

bytesPerRow := ((depth * (bounds.right - bounds.left) + 31) DIV 32) * 4;

This formula multiplies the number of pixels across the PixMap by the pixel depth to get the number of bits, and then this is divided by eight to get the number of bytes. This division by eight looks very strange because the number of bytes per row must be even, so this formula takes advantage of integer division and multiplication to make the result come out even. This particular formula additionally makes sure that the number of bytes per row is a multiple of four. This helps optimize the performance of Color QuickDraw operations because it allows Color QuickDraw to refer to each row beginning on a long word boundary in memory.

The last sanity check is to make sure that a color table is given as a parameter if it's needed. Indexed-color graphics environments need color tables, so if the given pixel depth is eight or less (which implies an indexed-color graphics environment) and the given color table is NIL, then CreateOffScreen decides that there's an error in the parameter list and does nothing more. If the given pixel depth is 16 or 32 (which implies a direct-color graphics environment), then CreateOffScreen ignores the given color table.

If all the sanity checks succeed, then the off-screen CGrafPort is allocated using a call to _NewPtr, and then it's initialized and opened as a CGrafPort by passing the resulting pointer to _OpenCPort. Because _OpenCPort makes the new CGrafPort the current port, the current port is first saved so that it can be restored as the current port when CreateOffScreen is done.

As mentioned above, the _OpenCPort doesn't necessarily initialize the portRect, visRgn, and clipRgn of the new CGrafPort to the areas that are needed for any particular off-screen graphics environment. So, the given boundary rectangle is assigned to the portRect field, _RectRgn is called to make the visRgn equal to the given boundary rectangle, and _ClipRect is called to set the clipRgn so that it's equal to the given boundary rectangle.

The PixMap in the portPixMap field needs to be initialized for off-screen drawing, and that's handled by the SetUpPixMap routine that's described and defined in "Building the PixMap" later in this Note. Similarly, the off-screen GDevice must be created and initialized. That's handled by the CreateGDevice routine that's described and defined in "Building the GDevice" later in this Note.

Once these things are done, CreateOffScreen returns a pointer to the off-screen CGrafPort in the retPort parameter and a handle to the off-screen GDevice in the retGDevice parameter. The way to use these references is described in "Playing With Blocks" later in this Note.

Building the PixMap

_OpenCPort initializes the portPixMap field of the CGrafPort it's initializing with a copy of the PixMap of the current GDevice. When the CreateOffScreen routine described earlier executes, the current GDevice is unknown. So, all the fields of the PixMap that the new CGrafPort receives must be initialized so that it can be used for drawing off screen.* What follows is an overview of each of the PixMap fields and how they should be initialized for off-screen drawing.

baseAddr pointer to the off-screen pixel image. The off-screen pixel image is allocated as a nonrelocatable block in the heap. The size of this block of memory is calculated from the rowBytes field, described next, multiplied by the number of rows in the given boundary rectangle.

rowBytes number of bytes in each row of the pixel image. This value is calculated from the formula that's given in the CreateOffScreen routine. The most significant bit of this field should be set so that Color QuickDraw knows that this is a PixMap rather than a BitMap. The maximum value, ignoring the most significant bit, is 16,382.

bounds defines the coordinate system and the dimensions of the pixel image. For most off-screen drawing, this should be a rectangle that covers the entire off-screen graphics environment.

pmVersion set of internally and externally defined flags. As of 32-Bit QuickDraw 1.2, only the baseAddr32 flag is defined externally. This flag is described in "Choosing Your Off-Screen Memory" later in this Note. For most off-screen drawing, this field is set to zero.

packType image compression scheme for pictures. The options for this field are discussed in the "Graphics Overview" chapter of Inside Macintosh Volume VI, pages 17-22 through 17-23. In this Note, image compression isn't discussed so this field is set to zero.

packSize internally used field. This field is always set to zero.

hRes horizontal resolution of the pixel map. By default, the QuickDraw resolution is 72 dots per inch,which is the value this Note uses. This is a fixed-point field, so the actual value in this field is $00480000.

vRes vertical resolution of the pixel map. See the hRes description.

pixelType format of the pixels. In indexed-color pixel maps, this field holds zero. In direct-color pixel maps, this field holds the RGBDirect constant, which is equal to 16.

pixelSize number of bits in every pixel. For indexed-color pixels, this is 1, 2, 4, or 8 bits per pixel. For direct-color pixels, this is 16 or 32 bits per pixel.

cmpCount number of components in every pixel. In indexed-color pixel maps, this field is set to 1. In direct-color pixel maps, this field is set to 3. Sometimes it's handy to set this field to 4 in 32-bit deep pixel maps when they're being saved in a picture. See the "Color QuickDraw" chapter of Inside Macintosh Volume VI, page 17-23, for details about this.

cmpSize number of bits in each color component. In indexed-color pixel maps, this field is set to the same value that's in the pixelSize field. In 16-bit deep direct pixel maps, this field is set to 5. In 32-bit deep direct pixel maps, this field is set to 8.

planeBytes not currently defined. This field is set to zero.

pmTable handle to the color table for indexed-color pixel maps. A method to create a color table is given in "About That Creation Thing . . ." later in this Note. In direct-color pixel maps, this field contains a handle to a dummy color table, and building one of these is shown in the SetUpPixMap routine in Listing 2.

pmReserved not currently defined. This field is set to zero. (*This part of these routines really bothers me because it feels impure to initialize all the PixMap fields when _OpenCPort has initialized them already, just not in a way that's any good for off-screen drawing. I tried creating the GDevice and PixMap first and then calling _OpenCPort so that it initializes its PixMap for off-screen drawing, but then you end up with two pixel maps and that makes this tougher to explain, or you have to dispose of one PixMap which seems worse than the method I'm using.)

The SetUpPixMap routine in Listing 2 initializes the PixMap that's passed to it in the aPixMap parameter so that it can be used in an off-screen graphics environment. The depth, bounds, and color parameters are the same as the ones passed to the CreateOffScreen routine. The bytesPerRow parameter is the number of bytes in each row of the off-screen pixel image. A description of SetUpPixMap follows the listing.

MPW Pascal Listing 2

FUNCTION SetUpPixMap(
   depth:       Integer;     {Desired number of bits/pixel in off-screen}
   bound:       Rect;        {Bounding rectangle of off-screen}
   colors:      CTabHandle;  {Color table to assign to off-screen}
   bytesPerRow: Integer;     {Number of bytes in each row of pixels}
   aPixMap:     PixMapHandle {Handle to the PixMap being initialized}
   ): OSErr;

   CONST
      kDefaultRes = $00480000; {Default resolution is 72 DPI; Fixed type}

   VAR
      newColors:   CTabHandle; {Color table used for the off-screen PixMap}
      offBaseAddr: Ptr;        {Pointer to the off-screen pixel image}
      error:       OSErr;      {Returns error code}

BEGIN
   error := noErr;
   newColors := NIL;
    offBaseAddr := NIL;

   (* Clone the clut if indexed color; allocate a dummy clut if direct color*)
   IF depth <= 8 THEN
      BEGIN
         newColors := colors;
         error := HandToHand(Handle(newColors));
      END
   ELSE
      BEGIN
         newColors := CTabHandle(NewHandle(SizeOf(ColorTable) -
               SizeOf(CSpecArray)));
         error := MemError;
      END;
   IF error = noErr THEN
      BEGIN
         (* Allocate pixel image; long integer multiplication avoids overflow*)
         offBaseAddr := NewPtr(LongInt(bytesPerRow) * (bound.bottom -
               bound.top));
         IF offBaseAddr <> NIL THEN
            WITH aPixMap^^ DO
               BEGIN
                  (* Initialize fields common to indexed and direct PixMaps*)
                  baseAddr := offBaseAddr;     {Point to image}
                  rowBytes := BOR(bytesPerRow, {MSB set for PixMap}
                        $8000);
                  bounds := bound;             {Use given bounds}
                  pmVersion := 0;              {No special stuff}
                  packType := 0;               {Default PICT pack}
                  packSize := 0;               {Always zero when in memory}
                  hRes := kDefaultRes;         {72 DPI default resolution}
                  vRes := kDefaultRes;         {72 DPI default resolution}
                  pixelSize := depth;          {Set number of bits/pixel}
                  planeBytes := 0;             {Not used}
                  pmReserved := 0;             {Not used}

                  (* Initialize fields specific to indexed and direct PixMaps*)
                  IF depth <= 8 THEN
                     BEGIN
                        (* PixMap is indexed *)
                        pixelType := 0;       {Indicates indexed}
                        cmpCount := 1;        {Have 1 component}
                        cmpSize := depth;     {Component size=depth}
                        pmTable := newColors; {Handle to CLUT}
                     END
                  ELSE
                     BEGIN
                        (* PixMap is direct *)
                        pixelType := RGBDirect; {Indicates direct}
                        cmpCount := 3;          {Have 3 components}
                        IF depth = 16 THEN
                           cmpSize := 5         {5 bits/component}
                        ELSE
                           cmpSize := 8;        {8 bits/component}

                        (* Initialize fields of the dummy color table *)
                        newColors^^.ctSeed := 3 * aPixMap^^.cmpSize;
                        newColors^^.ctFlags := 0;
                        newColors^^.ctSize := 0;
                        pmTable := newColors;
                     END;
               END
         ELSE
            error := MemError;
      END
   ELSE
      newColors := NIL;

   (* If no errors occurred, return a handle to the new off-screen PixMap *)
   IF error <> noErr THEN
      BEGIN
         IF newColors <> NIL THEN
            DisposCTable(newColors);
      END;

    (* Return the error code *)
    SetUpPixMap := error;
END;

MPW C Listing 2

#define kDefaultRes 0x00480000 /* Default resolution is 72 DPI; Fixed type */

OSErr SetUpPixMap(
    short        depth,       /* Desired number of bits/pixel in off-screen*/
    Rect         *bounds,     /* Bounding rectangle of off-screen */
    CTabHandle   colors,      /* Color table to assign to off-screen */
    short        bytesPerRow, /* Number of bytes per row in the PixMap */
    PixMapHandle aPixMap)     /* Handle to the PixMap being initialized */
{
    CTabHandle newColors;   /* Color table used for the off-screen PixMap */
    Ptr        offBaseAddr; /* Pointer to the off-screen pixel image */
    OSErr      error;       /* Returns error code */

    error = noErr;
    newColors = nil;
    offBaseAddr = nil;

    /* Clone the clut if indexed color; allocate a dummy clut if direct color*/
    if (depth <= 8)
    {
        newColors = colors;
        error = HandToHand( (Handle *)&newColors );
    }
    else
    {
        newColors = (CTabHandle)NewHandle( sizeof (ColorTable) -
                sizeof (CSpecArray) );
        error = MemError();
    }
    if (error == noErr)
    {
        /* Allocate pixel image; long integer multiplication avoids overflow*/
        offBaseAddr = NewPtr( (unsigned long)bytesPerRow * (bounds->bottom
                - bounds->top) );
        if (offBaseAddr != nil)
        {
            /* Initialize fields common to indexed and direct PixMaps */
            (**aPixMap).baseAddr = offBaseAddr;  /* Point to image */
            (**aPixMap).rowBytes = bytesPerRow | /* MSB set for PixMap */
                    0x8000;
            (**aPixMap).bounds = *bounds;        /* Use given bounds */
            (**aPixMap).pmVersion = 0;           /* No special stuff */
            (**aPixMap).packType = 0;            /* Default PICT pack */
            (**aPixMap).packSize = 0;            /* Always zero in mem */
            (**aPixMap).hRes = kDefaultRes;      /* 72 DPI default res */
            (**aPixMap).vRes = kDefaultRes;      /* 72 DPI default res */
            (**aPixMap).pixelSize = depth;       /* Set # bits/pixel */
            (**aPixMap).planeBytes = 0;          /* Not used */
            (**aPixMap).pmReserved = 0;          /* Not used */

            /* Initialize fields specific to indexed and direct PixMaps */
            if (depth <= 8)
            {
                /* PixMap is indexed */
                (**aPixMap).pixelType = 0;       /* Indicates indexed */
                (**aPixMap).cmpCount = 1;        /* Have 1 component */
                (**aPixMap).cmpSize = depth;     /* Component size=depth */
                (**aPixMap).pmTable = newColors; /* Handle to CLUT */
            }
            else
            {
                /* PixMap is direct */
                (**aPixMap).pixelType = RGBDirect; /* Indicates direct */
                (**aPixMap).cmpCount = 3;          /* Have 3 components */
                if (depth == 16)
                    (**aPixMap).cmpSize = 5;       /* 5 bits/component */
                else
                    (**aPixMap).cmpSize = 8;       /* 8 bits/component */
                (**newColors).ctSeed = 3 * (**aPixMap).cmpSize;
                (**newColors).ctFlags = 0;
                (**newColors).ctSize = 0;
                (**aPixMap).pmTable = newColors;
            }
        }
        else
            error = MemError();
    }
    else
        newColors = nil;

    /* If no errors occurred, return a handle to the new off-screen PixMap */
    if (error != noErr)
    {
        if (newColors != nil)
            DisposCTable( newColors );
    }

    /* Return the error code */
    return error;
}

SetUpPixMap begins by copying the given color table if an indexed-color graphics environment is being built, or allocating a dummy color table if a direct-color graphics environment is being built. A copy of the color table is made because this allows the given color table and the off-screen graphics environment's color table to be manipulated independently without interfering with each other, and this lets the off-screen graphics environment routines manipulate the color table without needing to worry about whether the color table is a 'clut' resource or not. The dummy color table is made so that routines which assume that every PixMap has a color table won't do something catastrophic if they find a NIL color table. The off-screen pixel image is then allocated as a nonrelocatable block in the application's heap.

Some of the fields of a PixMap have to be initialized differently depending upon whether the indexed-color model or the direct-color model is being used. So, the fields that are the same regardless of the color model that's being used are assigned first. Then the desired pixel depth is compared to 8. If the depth is less than or equal to 8, then the rest of the fields are initialized for the indexed-color model. Otherwise, the rest of the fields are initialized for the direct color model. In the case of the direct-color model, the dummy color table is initialized to have no CSpecArray entries and its ctSeed field is set to three times the component size. This dummy color table is then installed into the PixMap.

Once SetUpPixMap completes, the PixMap of the new CGrafPort is ready to hold an off-screen image. It's not quite ready to be drawn into with Color QuickDraw though. To do that, the off-screen GDevice is still needed; the construction and initialization of the GDevice are covered in the next section.

Building the GDevice

The _OpenCPort routine automatically allocates and initializes a PixMap, and the SetUpPixMap routine reinitializes that existing PixMap. _OpenCPort doesn't allocate nor initialize a GDevice, so one has to be created from scratch. Pages 21-20 through 21-21 of "The Graphics Devices Manager" chapter of Inside Macintosh Volume VI describe the _NewGDevice routine. This routine seems as though it's the ticket to getting a GDevice for off-screen drawing, but it always allocates the new GDevice in the system heap. That's not so good because if your program unexpectedly quits or if you just forget to dispose of the GDevice before you quit for real, the GDevice gets orphaned in the system heap. To prevent this from happening, _NewGDevice should be ignored and the off-screen GDevice should instead be allocated and initialized from scratch. What follows is a description of how each field of the GDevice structure should be initialized.

gdRefNum reference number of video driver. Off-screen graphics environments don't need to have video drivers because there's no video device associated with them, so this field is set to zero.

gdID used to identify specific GDevice structures from color-search procedures. This isn't necessary for off-screen drawing, so this is normally set to zero.

gdType type of GDevice. This field is set to the constant clutType (equal to zero) for an indexed-color environment and set to the constant directType (equal to 2) for a direct-color environment.

gdITable handle to the inverse table. Initially, this field is set to an arbitrarily small handle. Later, the _MakeITable routine is used to resize and initialize this handle to a real inverse table.

gdResPref inverse-table resolution. When _MakeITable is called by QuickDraw, the value of this field is used as the inverse-table resolution. Almost all inverse tables have a resolution of 4. There are some cases when a inverse-table resolution of 5 is useful, particularly when the arithmetic transfer modes are used with _CopyBits. See "The GDevice" earlier in this Note.

gdSearchProc pointer to the color-search procedure. If a color-search procedure is needed, this field can be set later by calling the _AddSearch routine (see the "Color Manager" chapter of Inside Macintosh Volume V, pages 145 through 147). Usually, this field is just set to NIL and left at that.

gdCompProc pointer to the color-complement procedure. If a color-complement procedure is needed, this field can be set later by calling the _AddComp routine (see the "Color Manager" chapter of Inside Macintosh Volume V, pages 145 through 147). Usually, this field is set to NIL and left at that.

gdFlags flags indicating certain states of the GDevice. This field should initially be set to zeroes. After the GDevice has been built, these flags can be set with the _SetDeviceAttrs routine (see the "Graphics Devices Manager" chapter of Inside Macintosh Volume VI, pages 21-10 and 21-22).

gdPMap handle to a PixMap. A handle to the PixMap of the CGrafPort that was created earlier is put into this field.

gdRefCon miscellaneous data. _CalcCMask and _SeedCFill use this field as described on pages 71 through 72 of Inside Macintosh Volume V. Initially, this field is set to zero.

gdNextGD handle to next GDevice in the GDevice list. The system maintains a linked list of GDevice records in which there's one GDevice for every screen, and the links are kept in this field. Off-screen GDevice structures should never be put into this list, so this field should be set to NIL.

gdRect rectangle of GDevice. Strictly speaking, this field is used only for screens, but it should be the same as the bounds rectangle of the off-screen PixMap.

gdMode current video mode. This field is used by video drivers to keep track of the current mode that the video device is in. For off-screen GDevice structures, this field should be set to -1.

gdCC... These four fields are used only with GDevice structures for screens. For off-screen GDevice structures, these fields should be set to zero.

gdReserved not currently defined. This field is set to zero.

The CreateGDevice routine shown below in Listing 3 allocates and initializes a GDevice structure. It takes the initialized off-screen PixMap in the basePixMap parameter and returns the initialized GDevice in the retGDevice parameter. If any error occurs, any memory that's allocated is disposed of and the result code is returned as a function result.

MPW Pascal Listing 3

FUNCTION CreateGDevice(
   basePixMap:     PixMapHandle; {Handle to the PixMap to base GDevice on}
   VAR retGDevice: GDHandle      {Returns a handle to the new GDevice}
   ): OSErr;

   CONST
      kITabRes = 4; {Inverse-table resolution}

   VAR
      newDevice:  GDHandle;   {Handle to the new GDevice}
      embryoITab: ITabHandle; {Handle to the embryonic inverse table}
      error:      OSErr;      {Error code}

BEGIN
   (* Initialize a few things before we begin *)
   error := noErr;
   newDevice := NIL;
   embryoITab := NIL;

   (* Allocate memory for the new GDevice *)
   newDevice := GDHandle(NewHandle(SizeOf(GDevice)));
   IF newDevice <> NIL THEN
      BEGIN
         (* Allocate the embryonic inverse table *)
         embryoITab := ITabHandle(NewHandleClear(2));
         IF embryoITab <> NIL THEN
            BEGIN
               (* Initialize the new GDevice fields *)
               WITH newDevice^^ DO
                  BEGIN
                     gdRefNum := 0;                 {Only used for screens}
                     gdID := 0;                     {Won't normally use}
                     IF basePixMap^^.pixelSize <= 8 THEN
                        gdType := clutType          {Depth<=8; clut device}
                     ELSE
                        gdType := directType;       {Depth>8; direct device}
                     gdITable := embryoITab;        {2-byte handle for now}
                     gdResPref := kITabRes;         {Normal inv table res}
                     gdSearchProc := NIL;           {No color-search proc}
                     gdCompProc := NIL;             {No complement proc}
                     gdFlags := 0;                  {Will set these later}
                     gdPMap := basePixMap;          {Reference our PixMap}
                     gdRefCon := 0;                 {Won't normally use}
                     gdNextGD := NIL;               {Not in GDevice list}
                     gdRect := basePixMap^^.bounds; {Use PixMap dimensions}
                     gdMode := -1;                  {For nonscreens}
                     gdCCBytes := 0;                {Only used for screens}
                     gdCCDepth := 0;                {Only used for screens}
                     gdCCXData := NIL;              {Only used for screens}
                     gdCCXMask := NIL;              {Only used for screens}
                     gdReserved := 0;               {Currently unused}
                  END;

               (* Set color-device bit if PixMap isn't black & white *)
               IF basePixMap^^.pixelSize > 1 THEN
                  SetDeviceAttribute(newDevice, gdDevType, true);

               (* Set bit to indicate that the GDevice has no video driver *)
               SetDeviceAttribute(newDevice, noDriver, true);

               (* Initialize the inverse table *)
               IF basePixMap^^.pixelSize <= 8 THEN
                  BEGIN
                     MakeITable(basePixMap^^.pmTable, newDevice^^.gdITable,
                           newDevice^^.gdResPref);
                     error := QDError;
                  END;
            END
         ELSE
            error := MemError;
      END
   ELSE
      error := MemError;

   (* Handle any errors along the way *)
   IF error <> noErr THEN
      BEGIN
         IF embryoITab <> NIL THEN
            DisposHandle(Handle(embryoITab));
         IF newDevice <> NIL THEN
            DisposHandle(Handle(newDevice));
      END
   ELSE
      retGDevice := newDevice;

   (* Return a handle to the new GDevice *)
   CreateGDevice := error;
END;

MPW C Listing 3

#define kITabRes 4 /* Inverse-table resolution */

OSErr CreateGDevice(
    PixMapHandle basePixMap,  /* Handle to the PixMap to base GDevice on */
    GDHandle     *retGDevice) /* Returns a handle to the new GDevice */
{
    GDHandle   newDevice;  /* Handle to the new GDevice */
    ITabHandle embryoITab; /* Handle to the embryonic inverse table */
    Rect       deviceRect; /* Rectangle of GDevice */
    OSErr      error;      /* Error code */

    /* Initialize a few things before we begin */
    error = noErr;
    newDevice = nil;
    embryoITab = nil;

    /* Allocate memory for the new GDevice */
    newDevice = (GDHandle)NewHandle( sizeof (GDevice) );
    if (newDevice != nil)
    {
        /* Allocate the embryonic inverse table */
        embryoITab = (ITabHandle)NewHandleClear( 2 );
        if (embryoITab != nil)
        {
            /* Set rectangle of device to PixMap bounds */
            deviceRect = (**basePixMap).bounds;

            /* Initialize the new GDevice fields */
            (**newDevice).gdRefNum = 0;            /* Only used for screens*/
            (**newDevice).gdID = 0;                /* Won't normally use */
            if ((**basePixMap).pixelSize <= 8)
                (**newDevice).gdType = clutType;   /* Depth<=8; clut device*/
            else
                (**newDevice).gdType = directType; /* Depth>8; direct device*/
            (**newDevice).gdITable = embryoITab;   /* 2-byte handle for now*/
            (**newDevice).gdResPref = kITabRes;    /* Normal inv table res */
            (**newDevice).gdSearchProc = nil;      /* No color-search proc */
            (**newDevice).gdCompProc = nil;        /* No complement proc */
            (**newDevice).gdFlags = 0;             /* Will set these later */
            (**newDevice).gdPMap = basePixMap;     /* Reference our PixMap */
            (**newDevice).gdRefCon = 0;            /* Won't normally use */
            (**newDevice).gdNextGD = nil;          /* Not in GDevice list */
            (**newDevice).gdRect = deviceRect;     /* Use PixMap dimensions*/
            (**newDevice).gdMode = -1;             /* For nonscreens */
            (**newDevice).gdCCBytes = 0;           /* Only used for screens*/
            (**newDevice).gdCCDepth = 0;           /* Only used for screens*/
            (**newDevice).gdCCXData = 0;           /* Only used for screens*/
            (**newDevice).gdCCXMask = 0;           /* Only used for screens*/
            (**newDevice).gdReserved = 0;          /* Currently unused */

            /* Set color-device bit if PixMap isn't black & white */
            if ((**basePixMap).pixelSize > 1)
                SetDeviceAttribute( newDevice, gdDevType, true );

            /* Set bit to indicate that the GDevice has no video driver */
            SetDeviceAttribute( newDevice, noDriver, true );

            /* Initialize the inverse table */
            if ((**basePixMap).pixelSize <= 8)
            {
                MakeITable( (**basePixMap).pmTable, (**newDevice).gdITable,
                        (**newDevice).gdResPref );
                error = QDError();
            }
        }
        else
            error = MemError();
    }
    else
        error = MemError();

    /* Handle any errors along the way */
    if (error != noErr)
    {
        if (embryoITab != nil)
            DisposHandle( (Handle)embryoITab );
        if (newDevice != nil)
            DisposHandle( (Handle)newDevice );
    }
    else
        *retGDevice = newDevice;

    /* Return a handle to the new GDevice */
    return error;
}

CreateGDevice begins by allocating the GDevice structure and an embryonic form of the inverse table in the current heap. The inverse table is allocated as two zero bytes for now; it'll be resized and initialized to be a real inverse table later in this routine. Then, each of the GDevice fields are initialized as described earlier.

After all the fields have been initialized, the gdFlags field is set through _SetDeviceAttribute. If the desired pixel depth is greater than 1, then the gdDevType bit is set. This indicates that the GDevice is for a color graphics environment. This bit should be set even if a gray-scale color table is used for this off-screen graphics environment. The noDriver bit is set because this is an off-screen GDevice and so there's no associated video device driver.

Finally, the inverse table is resized and initialized by calling the _MakeITable routine. A handle to the two-byte embryonic inverse table that was created earlier in CreateGDevice is passed as a parameter, as is a handle to the off-screen color table and the preferred inverse-table resolution.

All Fall Down

Now that we have a way to create an off-screen graphics environment, there has to be a way to get rid of it too. The DisposeOffScreen routine shown in Listing 4 does this. The CreateOffScreen routine returns an off-screen graphics environment that's represented by a CGrafPort and GDevice. The DisposeOffScreen routine takes the off-screen CGrafPort and GDevice and deallocates all the memory that's associated with them including the CGrafPort and its dependent structures, the GDevice, the PixMap, the color table, and the inverse table.

MPW Pascal Listing 4

PROCEDURE DisposeOffScreen(
   doomedPort:    CGrafPtr; {Pointer to the CGrafPort we're getting rid of}
   doomedGDevice: GDHandle  {Handle to the GDevice we're getting rid of}
   );

   VAR
      currPort:    CGrafPtr; {Pointer to the current port}
      currGDevice: GDHandle; {Handle to the current GDevice}

BEGIN
   (* Check to see whether the doomed CGrafPort is the current port *)
   GetPort(GrafPtr(currPort));
   IF currPort = doomedPort THEN
      BEGIN
         (* It is; set current port to Window Manager CGrafPort *)
         GetCWMgrPort(currPort);
         SetPort(GrafPtr(currPort));
      END;

   (* Check to see whether the doomed GDevice is the current GDevice *)
   currGDevice := GetGDevice;
   IF currGDevice = doomedGDevice THEN
      (* It is; set current GDevice to the main screen's GDevice *)
      SetGDevice(GetMainDevice);

   (* Throw everything away *)
   doomedGDevice^^.gdPMap := NIL;
   DisposGDevice(doomedGDevice);
   DisposPtr(doomedPort^.portPixMap^^.baseAddr);
   IF doomedPort^.portPixMap^^.pmTable <> NIL THEN
      DisposCTable(doomedPort^.portPixMap^^.pmTable);
   CloseCPort(doomedPort);
   DisposPtr(Ptr(doomedPort));
END;

MPW C Listing 4

void DisposeOffScreen(
    CGrafPtr doomedPort,    /* Pointer to the CGrafPort to be disposed of */
    GDHandle doomedGDevice) /* Handle to the GDevice to be disposed of */
{
    CGrafPtr currPort;    /* Pointer to the current port */
    GDHandle currGDevice; /* Handle to the current GDevice */

    /* Check to see whether the doomed CGrafPort is the current port */
    GetPort( (GrafPtr *)&currPort );
    if (currPort == doomedPort)
    {
        /* It is; set current port to Window Manager CGrafPort */
        GetCWMgrPort( &currPort );
        SetPort( (GrafPtr)currPort );
    }

    /* Check to see whether the doomed GDevice is the current GDevice */
    currGDevice = GetGDevice();
    if (currGDevice == doomedGDevice)
        /* It is; set current GDevice to the main screen's GDevice */
        SetGDevice( GetMainDevice() );

    /* Throw everything away */
    (**doomedGDevice).gdPMap = nil;
    DisposGDevice( doomedGDevice );
    DisposPtr( (**doomedPort->portPixMap).baseAddr );
    if ((**doomedPort->portPixMap).pmTable != nil)
        DisposCTable( (**doomedPort->portPixMap).pmTable );
    CloseCPort( doomedPort );
    DisposPtr( (Ptr)doomedPort );
}

One mildly tricky aspect of this is that we shouldn't dispose of the current graphics environment. To prevent this, the current port is retrieved by a call to _GetPort. If it returns a pointer to the same port that DisposeOffScreen is disposing, then the current port is set to the Window Manager's CGrafPort. That was an arbitrary choice, but it's the most neutral. Similarly, the current GDevice is retrieved by a call to _GetGDevice. If it returns a handle to the same GDevice that DisposeOffScreen is disposing, then the current port is set to the main screen's GDevice. Again, that's an arbitrary, neutral choice.

The inverse table, GDevice, pixel image, and color table are disposed of. Before disposing of the color table, a check is first made to see whether it's NIL. That's because it's reasonable, though not normal, for the PixMap not to have even a dummy color table if the direct-color model is being used. Then the CGrafPort is closed which deallocates all the pieces associated with the CGrafPort, including the PixMap. Once this is done, all the structures that were created by calling CreateOffScreen are deallocated.

Back to top

Playing With Blocks

Now that these four routines with two entry points can create and dispose of off-screen graphics environments, how are they used? There are several phases to using an off-screen graphics environment: creating it, drawing into it, switching between it and other off-screen and on-screen graphics environments, copying images to and from it, and disposing of it. Listing 5 shows a routine called ExerciseOffScreen which is a very basic example of all of these phases.

MPW Pascal Listing 5

PROCEDURE ExerciseOffScreen;

   CONST
      kOffDepth  = 8;    {Number of bits per pixel in off-screen environment}
      rGrayClut  = 1600; {Resource ID of gray-scale clut}
      rColorClut = 1601; {Resource ID of full-color clut}

   VAR
      grayPort:    CGrafPtr;   {Graphics environment for gray off screen}
      grayDevice:  GDHandle;   {Color environment for gray off screen}
      colorPort:   CGrafPtr;   {Graphics environment for color off screen}
      colorDevice: GDHandle;   {Color environment for color off screen}
      savedPort:   GrafPtr;    {Pointer to the saved graphics environment}
      savedDevice: GDHandle;   {Handle to the saved color environment}
      offColors:   CTabHandle; {Colors for off-screen environments}
      offRect:     Rect;       {Rectangle of off-screen environments}
      circleRect:  Rect;       {Rectangles for circle-drawing}
      count:       Integer;    {Generic counter}
      aColor:      RGBColor;   {Color used for drawing off screen}
      error:       OSErr;      {Error return from off-screen creation}

BEGIN
   (* Set up the rectangle for the off-screen graphics environments *)
   SetRect(offRect, 0, 0, 256, 256);

   (* Get the color table for the gray off-screen graphics environment *)
   offColors := GetCTable(rGrayClut);

   (* Create the gray off-screen graphics environment *)
   error := CreateOffScreen(offRect, kOffDepth, offColors, grayPort,
         grayDevice);

   IF error = noErr THEN
      BEGIN
         (* Get the color table for the color off-screen graphics environment*)
         offColors := GetCTable(rColorClut);

         (* Create the color off-screen graphics environment *)
         error := CreateOffScreen(offRect, kOffDepth, offColors, colorPort,
               colorDevice);

         IF error = noErr THEN
            BEGIN
               (* Save the current graphics environment *)
               GetPort(savedPort);
               savedDevice := GetGDevice;

               (* Set the current graphics environment to the gray one *)
               SetPort(GrafPtr(grayPort));
               SetGDevice(grayDevice);

               (* Draw gray-scale ramp into the gray off-screen environment*)
               FOR count := 0 TO 255 DO
                  BEGIN
                     aColor.red := count * 257;
                     aColor.green := aColor.red;
                     aColor.blue := aColor.green;
                     RGBForeColor(aColor);
                     MoveTo(0, count);
                     LineTo(255, count);
                  END;

               (* Copy gray ramp into color off-screen colorized with green*)
               SetPort(GrafPtr(colorPort));
               SetGDevice(colorDevice);
               aColor.red := $0000; aColor.green := $FFFF; aColor.blue :=$0000;
               RGBForeColor(aColor);
               CopyBits(GrafPtr(grayPort)^.portBits,
                     GrafPtr(colorPort)^.portBits,
                     grayPort^.portRect,
                     colorPort^.portRect,
                     srcCopy + ditherCopy, NIL);

               (* Draw red, green, and blue circles *)
               PenSize(8, 8);
               aColor.red := $FFFF; aColor.green := $0000; aColor.blue :=$0000;
               RGBForeColor(aColor);
               circleRect := colorPort^.portRect;
               FrameOval(circleRect);
               aColor.red := $0000; aColor.green := $FFFF; aColor.blue :=$0000;
               RGBForeColor(aColor);
               InsetRect(circleRect, 20, 20);
               FrameOval(circleRect);
               aColor.red := $0000; aColor.green := $0000; aColor.blue :=$FFFF;
               RGBForeColor(aColor);
               InsetRect(circleRect, 20, 20);
               FrameOval(circleRect);

               (* Copy the color off-screen environment to the current port*)
               SetPort(savedPort);
               SetGDevice(savedDevice);
               CopyBits(GrafPtr(colorPort)^.portBits, savedPort^.portBits,
                     colorPort^.portRect, savedPort^.portRect,
                     srcCopy, NIL);

               (* Dispose of the off-screen graphics environments *)
               DisposeOffScreen(grayPort, grayDevice);
               DisposeOffScreen(colorPort, colorDevice);
            END;
      END;
END;

MPW C Listing 5

#define kOffDepth  8    /* Number of bits per pixel in off-screen environment
*/
#define rGrayClut  1600 /* Resource ID of gray-scale clut */
#define rColorClut 1601 /* Resource ID of full-color clut */

void ExerciseOffScreen()
{
    CGrafPtr   grayPort;    /* Graphics environment for gray off screen */
    GDHandle   grayDevice;  /* Color environment for gray off screen */
    CGrafPtr   colorPort;   /* Graphics environment for color off screen */
    GDHandle   colorDevice; /* Color environment for color off screen */
    GrafPtr    savedPort;   /* Pointer to the saved graphics environment */
    GDHandle   savedDevice; /* Handle to the saved color environment */
    CTabHandle offColors;   /* Colors for off-screen environments */
    Rect       offRect;     /* Rectangle of off-screen environments */
    Rect       circleRect;  /* Rectangles for circle-drawing */
    short      count;       /* Generic counter */
    RGBColor   aColor;      /* Color used for drawing off screen */
    OSErr      error;       /* Error return from off-screen creation */

    /* Set up the rectangle for the off-screen graphics environments */
    SetRect( &offRect, 0, 0, 256, 256 );

    /* Get the color table for the gray off-screen graphics environment */
    offColors = GetCTable( rGrayClut );

    /* Create the gray off-screen graphics environment */
    error = CreateOffScreen( &offRect, kOffDepth, offColors,
            &grayPort, &grayDevice );

    if (error == noErr)
    {
        /* Get the color table for the color off-screen graphics environment*/
        offColors = GetCTable( rColorClut );

        /* Create the color off-screen graphics environment */
        error = CreateOffScreen( &offRect, kOffDepth, offColors,
                &colorPort, &colorDevice );

        if (error == noErr)
        {
            /* Save the current graphics environment */
            GetPort( &savedPort );
            savedDevice = GetGDevice();

            /* Set the current graphics environment to the gray one */
            SetPort( (GrafPtr)grayPort );
            SetGDevice( grayDevice );

            /* Draw gray-scale ramp into the gray off-screen environment */
            for (count = 0; count < 256; ++count)
            {
                aColor.red = aColor.green = aColor.blue = count * 257;
                RGBForeColor( &aColor );
                MoveTo( 0, count );
                LineTo( 255, count );
            }

            /* Copy gray ramp into color off-screen colorized with green */
            SetPort( (GrafPtr)colorPort );
            SetGDevice( colorDevice );
            aColor.red = 0x0000; aColor.green = 0xFFFF; aColor.blue = 0x0000;
            RGBForeColor( &aColor );
            CopyBits( &((GrafPtr)grayPort)->portBits,
                    &((GrafPtr)colorPort)->portBits,
                    &grayPort->portRect,
                    &colorPort->portRect,
                    srcCopy | ditherCopy, nil );

            /* Draw red, green, and blue circles */
            PenSize( 8, 8 );
            aColor.red = 0xFFFF; aColor.green = 0x0000; aColor.blue = 0x0000;
            RGBForeColor( &aColor );
            circleRect = colorPort->portRect;
            FrameOval( &circleRect );
            aColor.red = 0x0000; aColor.green = 0xFFFF; aColor.blue = 0x0000;
            RGBForeColor( &aColor );
            InsetRect( &circleRect, 20, 20 );
            FrameOval( &circleRect );
            aColor.red = 0x0000; aColor.green = 0x0000; aColor.blue = 0xFFFF;
            RGBForeColor( &aColor );
            InsetRect( &circleRect, 20, 20 );
            FrameOval( &circleRect );

            /* Copy the color off-screen environment to the current port */
            SetPort( savedPort );
            SetGDevice( savedDevice );
            CopyBits( &((GrafPtr)colorPort)->portBits,&savedPort->portBits,
                    &colorPort->portRect, &savedPort->portRect,
                    srcCopy, nil );

            /* Dispose of the off-screen graphics environments */
            DisposeOffScreen( grayPort, grayDevice );
            DisposeOffScreen( colorPort, colorDevice );
        }
    }
}

Two off-screen graphics environments are created in the same way. A rectangle that's 256 pixels wide by 256 pixels high and with its top-left coordinate at (0, 0) is created in the offRect local variable. 'clut' resources are loaded from the application's resource fork to use as the color tables of the two off-screen graphics environments; a gray-scale 'clut' in the first case and a full-color 'clut' in the second case. Then, CreateOffScreen is called with the rectangle, color table, and a hard-coded pixel depth of eight bits per pixel.

If CreateOffScreen returns noErr in both cases, then the current graphics environment is saved so that it can be restored later. Graphics environments consist of the current port and the current GDevice. The current GrafPort or CGrafPort is saved with_GetPort. The current GDevice is saved with _GetGDevice.

The gray-scale off-screen graphics environment is set as the current graphics environment by calling _SetPort with its CGrafPort and calling _SetGDevice with its GDevice. A vertical gray ramp is drawn into this graphics environment with the usual set of QuickDraw calls. This graphics environment's pixel image is then copied to the full-color off-screen graphics environment with dithering and colorization with green (dithering requires 32-Bit QuickDraw and consistent colorization requires system software version 7.0; both of these features are described in Konstantin Othmer's article "QuickDraw's CopyBits Procedure: Better Than Ever in System 7.0" in Issue 6 of develop). Before this copy happens, the full-color off-screen graphics environment must be set as the current one. Once this is done, _CopyBits can properly map colors from the gray-scale off-screen graphics environment to the full-color one which gets a green ramp image.

Red, green, and blue concentric circles are drawn into the full-color off-screen graphics environment over the green ramp. This image is then copied to the graphics environment that was the current one when ExerciseOffScreen was called. To do this, the saved graphics environment is set as the current one by what should now be the familiar calls to _SetPort and _SetGDevice. The off-screen image is then copied to the saved graphics environment with _CopyBits.

Finally, the two off-screen graphics environments are disposed of by calling the DisposeOffScreen routine that's defined in the section "All Fall Down" earlier in this Note.

Back to top

Put That Checkbook Away!

The previous section covered the basics of creating and using off-screen graphics environments. This is good enough for many, if not most, needs of off-screen drawing. But there are variations to creating and maintaining an off-screen graphics environment for specific cases. This section discusses a few of the more common cases.

About That Creation Thing . . .

The CreateOffScreen routine, defined in Listing 1, takes three pieces of information: the boundary rectangle, the desired pixel depth, and the desired color table. But there's much more to these pieces than ExerciseOffScreen shows. This section describes these pieces in more detail.

The first parameter to CreateOffScreen is a rectangle which determines the size and coordinate system of the off-screen graphics environment. Usually, the top-left corner of the rectangle has the coordinate (0, 0) because it's usually easiest to draw everything using coordinates that can also be thought of as the horizontal and vertical distance in pixels from the top-left corner of the graphics environment. But in some cases, it's more convenient to have the (0, 0) coordinate somewhere else, and passing CreateOffScreen a rectangle with a nonzero coordinate in the top-left corner is an easy way to do this. The coordinate system can be translated after the off-screen graphics environment is created by using the _SetOrigin routine that's described on pages 153 through 155 of Inside Macintosh Volume I.


Warning:
As Inside Macintosh Volume I, page 154, notes, the clip region of the port "sticks" to the coordinate system when you call _SetOrigin. If _SetOrigin offsets the coordinate system by a large amount, then the clip region might be moved completely outside of the port's drawing area, and nothing can be drawn into that port. After calling _SetOrigin, you should set the clip region so that you can continue drawing into the port.


The number of bits per pixel implies the maximum number of available colors in a graphics environment, at least roughly speaking. The relationship between the number of bits per pixel and the number of available colors is discussed in the "Graphics Overview" chapter of Inside Macintosh Volume VI, pages 16-8 through 16-9.

If an indexed-color graphics environment is being made, then a color table must be passed to CreateOffScreen. In ExerciseOffScreen, the color table is retrieved from a 'clut' resource that's in the application's resource fork with a call to _GetCTable. Because CreateOffScreen clones this color table, this 'clut' resource can be purgeable so that it can be thrown out if its memory is needed for other purposes. _GetCTable can also be passed some special constants that tell it to allocate various system color tables that can also be passed to CreateOffScreen. These special constants are described on page 17-18 of the "Color QuickDraw" chapter of Inside Macintosh Volume VI. _GetCTable allocates memory for these system color tables, so they should be disposed of after you're done with them.

A color table could also be built from scratch by allocating it with a call to _NewHandle and then initializing it by hand. The ColorTable structure is documented on pages 48 through 49 of Inside Macintosh Volume V. Here's what each of the fields should be set to:

ctSeed identification value. This is an arbitrary value that should be changed any time the contents of the color table change so that the inverse table can be kept current. When Color QuickDraw draws anything, it compares the ctSeed of the color table of the PixMap of the current GDevice against the iTabSeed field of the inverse table of the current GDevice. If they're the same, then Color QuickDraw uses colors according to that inverse table. If they're different, then Color QuickDraw first rebuilds the inverse table according to the new color table's contents and its iTabSeed is set to the value of the new color table's ctSeed; then the rebuilt inverse table is used.

When _CopyBits is called with the srcCopy transfer mode, the ctSeed fields of the source and destination pixel maps are compared. If they're the same, then _CopyBits simply transfers the source pixels to the destination with no mapping of colors. If they're different, then _CopyBits checks each entry of the color tables to determine whether they have the same colors for the same pixel values. If they do, then _CopyBits again simply transfers the source pixels to the destination with no mapping of colors. If they don't, then _CopyBits maps colors in the source PixMap to the colors in the current graphics environment according to the inverse table of the current GDevice. The ctSeed field of a color table should be changed whenever its contents are changed so that _CopyBits doesn't make the wrong assumptions about the equality of the source and destination color tables.

You can get a seed value for a new color table by assigning to it the result of the _GetCTSeed routine, documented in the "Color Manager" chapter of Inside Macintosh Volume V, page 143. If the contents of an existing color table are changed, then it should be passed to the _CTabChanged routine which assigns a new value to its ctSeed field. If the _CTabChanged routine isn't available (it's available with 32-Bit QuickDraw and is included with the system beginning with system software version 7.0), then the ctSeed field should be given a new value with another call to _GetCTSeed.

ctFlags indicates the Boolean characteristics of a color table. If the most significant bit of ctFlags is clear, then the value field of each ColorSpec entry in the ctTable array is interpreted as the pixel value for the color that's specified in the rgb field in the same ColorSpec entry. You can build a color table with nonconsecutive pixel values this way. If this bit is set, then all the value fields in the color table are ignored and the index of each ColorSpec record in the ctTable array is that record's pixel value. It's your choice whether to clear this bit and set the value fields or set this bit and ignore the value fields; traditionally this bit is clear for off-screen color tables.

If the next most significant bit of ctFlags is set, then the value field of each ColorSpec record in the ctTable array is used by _CopyBits as an index into the color palette that's attached to the destination window, and the rgb field is ignored. This is documented in the "Palette Manager" chapter of Inside Macintosh Volume VI, page 20-17.

The other bits are reserved for future use. If you create a color table from scratch, these other bits must be set to zero. If you use a color table that's generated by the system, then these bits must be preserved.

ctSize the number of color table entries minus 1. Normally, this field is set to 1, 3, 15, or 255 for 1-, 2-, 4-, and 8-bits per pixel, respectively. In special cases, it's reasonable to have less than the maximum number of entries for the pixel depth. For example, a color table for an 8-bit per pixel graphics environment could have just 150 entries, in which case the ctSize field should hold 149. For this case, it's still important to allocate as much space in the color table for the maximum number of entries for a pixel depth and clear the entries you're not using to zero because some parts of Color QuickDraw assume the size of a color table based on the pixel depth.

ctTable array of colors and pixel values. This table defines all the available colors in the color table and their pixel values. The value field of each ColorSpec record indicates that color's pixel value if the most significant bit of ctFlags is clear. It's ignored if the most significant bit of ctFlags is set. The value field is used as an index into a palette if the next most significant bit of ctFlags is set, in which case the rgb field is ignored. See the discussion of the ctFlags field earlier in this Note for more details.


Warning:
Color QuickDraw's text-drawing routines assume that the color table of the destination graphics environment has the maximum number of colors for the pixel depth of the graphics environment, and that white is the first entry in the color table and black is the last entry. If these conditions aren't satisfied, then the resulting image is unpredictable.


The code fragment in Listing 6 shows how to allocate a 256-entry color table from scratch. Color tables have a variable size, so the _NewHandle call has to calculate the size of the ColorTable record plus the maximum number of color table entries for the pixel depth multiplied by the size of a ColorSpec record. kNumColors - 1 is used in the calculation because the size of the ColorTable record includes the size of one ColorSpec entry in most development environments.

MPW Pascal Listing 6

CONST
   kNumColors = 256; {Number of color table entries}

VAR
   newColors: CTabHandle; {Handle to the new color table}
   index:     Integer;    {Index into the table of colors}

(* Allocate memory for the color table *)
newColors := CTabHandle(NewHandleClear(SizeOf (ColorTable) +
      SizeOf(ColorSpec) * (kNumColors - 1)));
IF newColors <> NIL THEN
   BEGIN
      (* Initialize the fields *)
      newColors^^.ctSeed := GetCTSeed;
      newColors^^.ctFlags := 0;
      newColors^^.ctSize := kNumColors - 1;

      (* Initialize the table of colors *)
      FOR index := 0 TO kNumColors - 1 DO
         BEGIN
            newColors^^.ctTable[index].value := index;
            newColors^^.ctTable[index].rgb.red := someRedValue;
            newColors^^.ctTable[index].rgb.green := someGreenValue;
            newColors^^.ctTable[index].rgb.blue := someBlueValue
         END
   END

MPW C Listing 6

#define kNumColors 256 /* Number of color table entries */

CTabHandle newColors; /* Handle to the new color table */
short      index;     /* Index into the table of colors */

/* Allocate memory for the color table */
newColors = (CTabHandle)NewHandleClear( sizeof (ColorTable) +
        sizeof (ColorSpec) * (kNumColors - 1) );
if (newColors != nil)
{
    /* Initialize the fields */
    (**newColors).ctSeed = GetCTSeed();
    (**newColors).ctFlags = 0;
    (**newColors).ctSize = kNumColors - 1;

    /* Initialize the table of colors */
    for (index = 0; index < kNumColors; index++)
    {
        (**newColors).ctTable[index].value = index;
        (**newColors).ctTable[index].rgb.red = someRedValue;
        (**newColors).ctTable[index].rgb.green = someGreenValue;
        (**newColors).ctTable[index].rgb.blue = someBlueValue;
    }
}

Changing Your Environment

After you create an off-screen graphics environment with certain dimensions, you might later want to change its size, depth, or color table without creating a completely new graphics environment from scratch and without needing to redraw the existing image. The UpdateOffScreen routine in Listing 7 shows just one way to do this. It takes the same parameters that CreateOffScreen (defined in Listing 1) does, but instead of creating a new CGrafPort and GDevice, it alters the ones that you pass through the updPort and updGDevice parameters. If the newBounds parameter specifies an empty rectangle, then the existing boundary rectangle for the off-screen graphics environment is used. Similarly, if newDepth is zero, then the existing depth is used; and if the newColors parameter is NIL, then the existing color table is used. UpdateOffScreen alters the given CGrafPort and GDevice to the new settings, but it completely replaces the PixMap. After all the alterations are made, the old PixMap's image is copied to the new PixMap's image, and then the old PixMap and its image are disposed.

MPW Pascal Listing 7

FUNCTION UpdateOffScreen(
   newBounds:  Rect;       {New bounding rectangle of off-screen}
   newDepth:   Integer;    {New number of bits per pixel in off-screen}
   newColors:  CTabHandle; {New color table to assign to off-screen}
   updPort:    CGrafPtr;   {Returns a pointer to the updated CGrafPort}
   updGDevice: GDHandle    {Returns a handle to the updated GDevice}
   ): OSErr;

   CONST
      kMaxRowBytes = $3FFE; {Maximum number of bytes per row of pixels}

   VAR
      newPixMap:   PixMapHandle; {Handle to the new off-screen PixMap}
      oldPixMap:   PixMapHandle; {Handle to the old off-screen PixMap}
      bounds:      Rect;         {Boundary rectangle of off-screen}
      depth:       Integer;      {Depth of the off-screen PixMap}
      bytesPerRow: Integer;      {Number of bytes per row in the PixMap}
      colors:      CTabHandle;   {Colors for the off-screen PixMap}
      savedFore:   RGBColor;     {Saved foreground color}
      savedBack:   RGBColor;     {Saved background color}
      aColor:      RGBColor;     {Used to set foreground and backgroundcolor}
      qdVersion:   LongInt;      {Version of QuickDraw currently in use}
      savedPort:   GrafPtr;      {Pointer to GrafPort used for save/restore}
      savedDevice: GDHandle;     {Handle to GDevice used for save/restore}
      savedState:  SignedByte;   {Saved state of color table handle}
      error:       OSErr;        {Returns error code}

BEGIN
   (* Initialize a few things before we begin *)
   newPixMap := NIL;
   error := noErr;

   (* Keep the old bounds rectangle, or get the new one *)
   IF EmptyRect(newBounds) THEN
      bounds := updPort^.portRect
   ELSE
      bounds := newBounds;

   (* Keep the old depth, or get the old one *)
   IF newDepth = 0 THEN
      depth := updPort^.portPixMap^^.pixelSize
   ELSE
      depth := newDepth;

   (* Get the old clut, or save new clut's state and make it nonpurgeable *)
   IF newColors = NIL THEN
      colors := updPort^.portPixMap^^.pmTable
   ELSE
      BEGIN
         savedState := HGetState(Handle(newColors));
         HNoPurge(Handle(newColors));
         colors := newColors;
      END;

   (* Calculate the number of bytes per row in the off-screen PixMap *)
   bytesPerRow := ((depth * (bounds.right - bounds.left) + 31) DIV 32) * 4;

   (* Get the current QuickDraw version *)
   error := Gestalt (gestaltQuickdrawVersion, qdVersion);
   error := noErr;

   (* Make sure depth is indexed or depth is direct and 32-Bit QD installed*)
   IF (depth = 1) OR (depth = 2) OR (depth = 4) OR (depth = 8) OR
         (((depth = 16) OR (depth = 32)) AND (qdVersion >= gestalt32BitQD))
THEN
      BEGIN
         (* Maximum number of bytes per row is 16,382; make sure within range*)
         IF bytesPerRow <= kMaxRowBytes THEN
            BEGIN
               (* Make sure a color table is provided if the depth is indexed*)
               IF depth <= 8 THEN
                  IF colors = NIL THEN
                     (* Indexed depth and clut is NIL; is parameter error *)
                     error := paramErr;
            END
         ELSE
            (* # of bytes per row is more than 16,382; is parameter error *)
            error := paramErr;
      END
   ELSE
      (* Pixel depth isn't valid; is parameter error *)
      error := paramErr;

   (* If sanity checks succeed, attempt to update the graphics environment *)
   IF error = noErr THEN
      BEGIN
         (* Allocate a new PixMap *)
         newPixMap := PixMapHandle(NewHandleClear(SizeOf(PixMap)));
         IF newPixMap <> NIL THEN
              BEGIN
               (* Initialize the new PixMap for off-screen drawing *)
               error := SetUpPixMap(depth, bounds, colors, bytesPerRow,
                     newPixMap);
               IF error = noErr THEN
                  BEGIN
                     (* Save old PixMap and install new, initialized one *)
                     oldPixMap := updPort^.portPixMap;
                     updPort^.portPixMap := newPixMap;

                     (* Save current port & GDevice; set ones we're updating *)
                     GetPort(savedPort);
                     savedDevice := GetGDevice;
                     SetPort(GrafPtr(updPort));
                     SetGDevice(updGDevice);

                     (* Set portRect, visRgn, clipRgn to given bounds rect *)
                     updPort^.portRect := bounds;
                     RectRgn(updPort^.visRgn, bounds);
                     ClipRect(bounds);

                     (* Update the GDevice *)
                     IF newPixMap^^.pixelSize <= 8 THEN
                        updGDevice^^.gdType := clutType
                     ELSE
                        updGDevice^^.gdType := directType;
                     updGDevice^^.gdPMap := newPixMap;
                     updGDevice^^.gdRect := newPixMap^^.bounds;

                     (* Set color-device bit if PixMap isn't black & white*)
                     IF newPixMap^^.pixelSize > 1 THEN
                        SetDeviceAttribute(updGDevice, gdDevType, TRUE);
                     else
                        SetDeviceAttribute(updGDevice, gdDevType, FALSE);

                     (* Save current fore/back colors and set to B&W *)
                     GetForeColor(savedFore);
                     GetBackColor(savedBack);
                     aColor.red := 0; aColor.green := 0; aColor.blue := 0;
                     RGBForeColor(aColor);
                     aColor.red := $FFFF;
                     aColor.green := $FFFF;
                     aColor.blue := $FFFF;
                     RGBBackColor(aColor);

                     (* Copy old image to the new graphics environment *)
                     HLock(Handle(oldPixMap));
                     CopyBits(BitMapPtr(oldPixMap^)^,GrafPtr(updPort)^.portBits,
                           oldPixMap^^.bounds, updPort^.portRect,
                           srcCopy, NIL);
                     HUnlock(Handle(oldPixMap));

                     (* Restore the foreground/background color *)
                     RGBForeColor(savedFore);
                     RGBBackColor(savedBack);

                     (* Restore the saved port *)
                     SetPort(savedPort);
                     SetGDevice(savedDevice);

                     (* Get rid of the old PixMap and its dependents *)
                     DisposPtr(oldPixMap^^.baseAddr);
                     DisposeCTable(oldPixMap^^.pmTable);
                     DisposHandle(Handle(oldPixMap));
                  END;
            END
         ELSE
              error := MemError;
     END;

   (* Restore the given state of the color table *)
   IF colors <> NIL THEN
      HSetState(Handle(colors), savedState);

   (* One Last Look Around The House Before We Go... *)
   IF error <> noErr THEN
      BEGIN
         IF newPixMap <> NIL THEN
            BEGIN
               IF newPixMap^^.pmTable <> NIL THEN
                  DisposCTable(newPixMap^^.pmTable);
               IF newPixMap^^.baseAddr <> NIL THEN
                  DisposPtr(newPixMap^^.baseAddr);
               DisposHandle(Handle(newPixMap));
            END;
      END;
   UpdateOffScreen := error;
END;

MPW C Listing 7

#define kMaxRowBytes 0x3FFE /* Maximum number of bytes in a row of pixels */

OSErr UpdateOffScreen(
    Rect       *newBounds, /* New bounding rectangle of off-screen */
    short      newDepth,   /* New number of bits per pixel in off-screen */
    CTabHandle newColors,  /* New color table to assign to off-screen */
    CGrafPtr   updPort,    /* Returns a pointer to the updated CGrafPort */
    GDHandle   updGDevice) /* Returns a handle to the updated GDevice */
{
    PixMapHandle newPixMap;   /* Handle to the new off-screen PixMap */
    PixMapHandle oldPixMap;   /* Handle to the old off-screen PixMap */
    Rect         bounds;      /* Boundary rectangle of off-screen */
    short        depth;       /* Depth of the off-screen PixMap */
    short        bytesPerRow; /* Number of bytes per row in the PixMap */
    CTabHandle   colors;      /* Colors for the off-screen PixMap */
    RGBColor     savedFore;   /* Saved foreground color */
    RGBColor     savedBack;   /* Saved background color */
    RGBColor     aColor;      /* Used to set foreground and background color*/
    long         qdVersion;   /* Version of QuickDraw currently in use */
    GrafPtr      savedPort;   /* Pointer to GrafPort used for save/restore */
    GDHandle     savedDevice; /* Handle to GDevice used for save/restore */
    SignedByte   savedState;  /* Saved state of color table handle */
    OSErr        error;       /* Returns error code */

    /* Initialize a few things before we begin */
    newPixMap = nil;
    error = noErr;

    /* Keep the old bounds rectangle, or get the new one */
    if (EmptyRect( newBounds ))
        bounds = updPort->portRect;
    else
        bounds = *newBounds;

    /* Keep the old depth, or get the old one */
    if (newDepth == 0)
        depth = (**updPort->portPixMap).pixelSize;
    else
        depth = newDepth;

    /* Get the old clut, or save new clut's state and make it nonpurgeable */
    if (newColors == nil)
        colors = (**updPort->portPixMap).pmTable;
    else
    {
        savedState = HGetState( (Handle)newColors );
        HNoPurge( (Handle)newColors );
        colors = newColors;
    }

    /* Calculate the number of bytes per row in the off-screen PixMap */
    bytesPerRow = ((depth * (bounds.right - bounds.left) + 31) >> 5)<< 2;

    /* Get the current QuickDraw version */
    (void)Gestalt( gestaltQuickdrawVersion, &qdVersion );

    /* Make sure depth is indexed or depth is direct and 32-Bit QD installed*/
    if (depth == 1 || depth == 2 || depth == 4 || depth == 8 ||
            ((depth == 16 || depth == 32) && qdVersion >=gestalt32BitQD))
    {
        /* Maximum number of bytes per row is 16,382; make sure within range*/
        if (bytesPerRow <= kMaxRowBytes)
        {
            /* Make sure a color table is provided if the depth is indexed */
            if (depth <= 8)
                if (colors == nil)
                    /* Indexed depth and clut is NIL; is parameter error */
                    error = paramErr;
        }
        else
            /* # of bytes per row is more than 16,382; is parameter error */
            error = paramErr;
    }
    else
        /* Pixel depth isn't valid; is parameter error */
        error = paramErr;

    /* If sanity checks succeed, attempt to create a new graphics environment*/
    if (error == noErr)
    {
        /* Allocate a new PixMap */
        newPixMap = (PixMapHandle)NewHandleClear( sizeof (PixMap) );
        if (newPixMap != nil)
        {
            /* Initialize the new PixMap for off-screen drawing */
            error = SetUpPixMap( depth, &bounds, colors, bytesPerRow, newPixMap );
            if (error == noErr)
            {
                /* Save the old PixMap and install the new, initialized one*/
                oldPixMap = updPort->portPixMap;
                updPort->portPixMap = newPixMap;

                /* Save current port & GDevice and set ones we're updating*/
                GetPort( &savedPort );
                savedDevice = GetGDevice();
                SetPort( (GrafPtr)updPort );
                SetGDevice( updGDevice );

                /* Set portRect, visRgn, and clipRgn to the given bounds rect*/
                updPort->portRect = bounds;
                RectRgn( updPort->visRgn, &bounds );
                ClipRect( &bounds );

                /* Update the GDevice */
                if ((**newPixMap).pixelSize <= 8)
                    (**updGDevice).gdType = clutType;
                else
                    (**updGDevice).gdType = directType;
                (**updGDevice).gdPMap = newPixMap;
                (**updGDevice).gdRect = (**newPixMap).bounds;

                /* Set color-device bit if PixMap isn't black & white */
                if ((**newPixMap).pixelSize > 1)
                    SetDeviceAttribute( updGDevice, gdDevType, true );
                else
                    SetDeviceAttribute( updGDevice, gdDevType, false );

                /* Save current foreground/background colors and set to B&W*/
                GetForeColor( &savedFore );
                GetBackColor( &savedBack );
                aColor.red = aColor.green = aColor.blue = 0;
                RGBForeColor( &aColor );
                aColor.red = aColor.green = aColor.blue = 0xFFFF;
                RGBBackColor( &aColor );

                /* Copy old image to the new graphics environment */
                HLock( (Handle)oldPixMap );
                CopyBits( (BitMapPtr)*oldPixMap, &((GrafPtr)updPort)->portBits,
                        &(**oldPixMap).bounds, &updPort->portRect,
                        srcCopy, nil );
                HUnlock( (Handle)oldPixMap );

                /* Restore the foreground/background color */
                RGBForeColor( &savedFore );
                RGBBackColor( &savedBack );

                /* Restore the saved port */
                SetPort( savedPort );
                SetGDevice( savedDevice );

                /* Get rid of the old PixMap and its dependents */
                DisposPtr( (**oldPixMap).baseAddr );
                DisposeCTable( (**oldPixMap).pmTable ) ;
                DisposHandle( (Handle)oldPixMap );
            }
        }
        else
            error = MemError();
    }

    /* Restore the given state of the color table */
    if (colors != nil)
        HSetState( (Handle)colors, savedState );

    /* One Last Look Around The House Before We Go... */
    if (error != noErr)
    {
        /* Some error occurred; dispose of everything we allocated */
        if (newPixMap != nil)
        {
            if ((**newPixMap).pmTable)
                DisposCTable( (**newPixMap).pmTable );
            if ((**newPixMap).baseAddr)
                DisposPtr ( (**newPixMap).baseAddr );
            DisposHandle( (Handle)newPixMap );
        }
    }
    return error;
}

UpdateOffScreen begins by checking the boundary rectangle, depth, or color table for emptiness, zero, or NIL, respectively. If any these satisfy that condition, then the existing characteristic is used. Next, the same sanity check that CreateOffScreen uses is done. If this sanity check succeeds, then a new PixMap is allocated, and then it's initialized by the SetUpPixMap routine that's given in Listing 2 which gives the new PixMap a new pixel image and its own copy of the color table. This new PixMap is installed into the CGrafPort after saving the reference to the old PixMap. Then, the portRect, visRgn, and clipRgn of the CGrafPort are set to the new boundary rectangle, as is the gdRect of the GDevice. The gdType of the GDevice is set either for the indexed-color or direct-color model, the gdPMap is set to the new PixMap, and the device attributes are set according to the pixel depth. Details about the settings for the CGrafPort and GDevice are in "Building the CGrafPort" and "Building the GDevice," respectively, earlier in this Note.

At this point, the off-screen graphics environment is ready with its new characteristics, but it has garbage for an image because nothing has been drawn into it yet. The old PixMap, pixel image, and color table are still around, so _CopyBits transfers the old image into the altered graphics environment. _CopyBits handles the mapping from the old image's characteristics to the new characteristics, so the altered graphics environment gets the best possible representation of the old image according to its new characteristics.

Changing the Off-Screen Color Table

Sometimes, it's useful to change some or all of the colors in an off-screen color table, or to replace the off-screen color table with another one, so that the existing image in an indexed-color graphics environment appears with new colors. For example, if you had an off-screen image of a blue car and wanted to see what it looked like in green, you could change all of the shades of blue in the off-screen color table to green, and then _CopyBits the image to the screen. Notice that this is different from calling the UpdateOffScreen routine in the previous section with a different color table. That routine tries to reproduce the colors from the original image as best it can in the new set of colors. This section discusses the case in which you want the image's colors to change.

The most obvious part of doing this is simply to get the color table from the off-screen pixel map's pmTable field and modify the entries, or to dispose of the off-screen graphics environment's current color table and assign the new one to it. There's one more step to complete the process though. The discussion about GDevice records in "The Building Blocks" in this Note discusses inverse tables and how they go hand-in-hand with color tables. If you alter or replace the color table, you have to make sure that the inverse table of the off-screen drawing environment is rebuilt according to the new colors because Color QuickDraw uses that inverse table to know what pixel values to use for the specified color. You don't have to rebuild the inverse table explicitly as long as you tell Color QuickDraw that the color table changed. To do this, all you have to do is make sure that the ctSeed of the changed or altered color table is set to a new value. And to do this, you can simply call _CTabChanged, which is documented on page 17-26 of the "Color QuickDraw" chapter of Inside Macintosh Volume VI. _CTabChanged is available beginning with 32-Bit QuickDraw and it's available in system software version 7.0. If this routine isn't available, then you can still tell Color QuickDraw that the color table has been changed by calling _GetCTSeed and assigning its result directly to your new color table's ctSeed field.

The next time you draw into this off-screen drawing environment, Color QuickDraw checks the ctSeed of the environment's color table against the iTabSeed of the inverse table of the environment's GDevice. Because you changed the ctSeed of the color table either through _CTabChanged or _GetCTSeed, these two seeds are different so Color QuickDraw automatically rebuilds the inverse table of the current GDevice and then it copies the ctSeed of the color table to the iTabSeed of the rebuilt inverse table. Then drawing continues normally.

Follow That Screen!

One common need of off-screen graphics environments is that they have a depth and color table that matches a screen. The CreateOffScreen routine requires a color table for indexed-color environments, and a pixel depth. Because there can be more than one screen attached to a Macintosh system, you have to decide which screen's depth and color table you should use. Typically, the depth and color table of the deepest screen that contains the area that you're interested in (probably the area of a window) is used. Another option is to use the depth and color table of the screen that has the largest area of intersection with the area that you're interested in. To find the depth and color table of the screen on which you want to base an off-screen graphics environment, you must use the list of graphics devices for all screens which is maintained by the system. Every GDevice record for a screen has a handle to that screen's PixMap, and you can find the screen's depth and color table there.

Listing 8 shows a routine called CreateScreenOffScreen which creates an off-screen graphics environment that has the depth and color table of a selected screen. The first parameter, bounds, specifies the rectangular part of the screen area in which you're interested in global coordinates. The screenOption parameter specifies how you want the screen to be chosen. If you pass kDeepestScreen in this parameter, CreateScreenOffScreen creates the new off-screen graphics environment with the depth and color table of the deepest screen that intersects the bounds rectangle. If you instead pass kLargestScreenArea, then the new off-screen graphics environment is created with the depth and color table of the screen with the largest area of intersection with the bounds rectangle.

MPW Pascal Listing 8

TYPE
   ScreenOpt = (kDeepestScreen, kLargestAreaScreen);

FUNCTION CreateScreenOffScreen(
   bounds:         Rect;      {Global rectangle of part of screen to save}
   screenOption:   ScreenOpt; {Use deepest or largest intersection area screen?}
   VAR retPort:    CGrafPtr;  {Returns a pointer to the new CGrafPort}
   VAR retGDevice: GDHandle   {Returns a handle to the new GDevice}
   ): OSErr;

   VAR
      baseGDevice:  GDHandle;     {GDevice to base off-screen on}
      aGDevice:     GDHandle;     {Handle to each GDevice in the GDevice list}
      basePixMap:   PixMapHandle; {baseGDevice's PixMap}
      maxArea:      LongInt;      {Largest intersection area found}
      area:         LongInt;      {Area of rectangle of intersection}
      commonRect:   Rect;         {Rectangle of intersection}
      normalBounds: Rect;         {bounds rectangle normalized to (0, 0)}
      error:        Integer;      {Error code}

BEGIN
   error := noErr;

   (* Different screen options require different algorithms *)
   IF screenOption = kDeepestScreen THEN
      (* Graphics Devices Manager tells us the deepest intersecting screen *)
      baseGDevice := GetMaxDevice(bounds)
   ELSE IF screenOption = kLargestAreaScreen THEN
      BEGIN
         (* Get a handle to the first GDevice in the GDevice list *)
         aGDevice := GetDeviceList;

         (* Keep looping until all GDevices have been checked *)
         maxArea := 0;
         baseGDevice := NIL;
         WHILE aGDevice <> NIL DO
            BEGIN
               (* Check to see whether screen rectangle and bounds intersect*)
               IF SectRect(aGDevice^^.gdRect, bounds, commonRect) THEN
                  BEGIN
                     (* Calculate area of intersection *)
                     area := LongInt(commonRect.bottom - commonRect.top) *
                           LongInt(commonRect.right - commonRect.left);

                     (* Keep track of largest area of intersection so far *)
                     IF area > maxArea THEN
                        BEGIN
                           maxArea := area;
                           baseGDevice := aGDevice;
                        END;
                  END;

               (* Go to the next GDevice in the GDevice list *)
               aGDevice := GetNextDevice(aGDevice);
            END;
      END
   ELSE
      error := paramErr;

   (* If no screens intersect the bounds, baseDevice is NIL *)
   IF (baseGDevice <> NIL) AND (error = noErr) THEN
      BEGIN
         (* Normalize the bounds rectangle *)
         normalBounds := bounds;
         OffsetRect(normalBounds, -normalBounds.left, -normalBounds.top);

         (* Create off-screen graphics environment w/ depth, clut of screen*)
         basePixMap := baseGDevice^^.gdPMap;
         error := CreateOffScreen(normalBounds, basePixMap^^.pixelSize,
               basePixMap^^.pmTable, retPort, retGDevice);
      END;
   CreateScreenOffScreen := error;
END;

MPW C Listing 8

enum
{
    kDeepestScreen,
    kLargestAreaScreen,
};

OSErr CreateScreenOffScreen(
    Rect     *bounds,      /* Global rectangle of part of screen to save */
    short    screenOption, /* Use deepest or largest intersection area screen*/
    CGrafPtr *retPort,     /* Returns a pointer to the new CGrafPort */
    GDHandle *retGDevice)  /* Returns a handle to the new GDevice */
{
    GDHandle     baseGDevice;  /* GDevice to base off-screen on */
    GDHandle     aGDevice;     /* Handle to each GDevice in the GDevice list*/
    PixMapHandle basePixMap;   /* baseGDevice's PixMap */
    long         maxArea;      /* Largest intersection area found */
    long         area;         /* Area of rectangle of intersection */
    Rect         commonRect;   /* Rectangle of intersection */
    Rect         normalBounds; /* bounds rectangle normalized to (0, 0) */
    short        error;        /* Error code */

    error = noErr;

    /* Different screen options require different algorithms */
    if (screenOption == kDeepestScreen)
        /* Graphics Devices Manager tells us the deepest intersecting screen */
        baseGDevice = GetMaxDevice( bounds );
    else if (screenOption == kLargestAreaScreen)
    {
        /* Get a handle to the first GDevice in the GDevice list */
        aGDevice = GetDeviceList();

        /* Keep looping until all GDevices have been checked */
        maxArea = 0;
        baseGDevice = nil;
        while (aGDevice != nil)
        {
            /* Check to see whether screen rectangle and bounds intersect */
            if (SectRect( &(**aGDevice).gdRect, bounds, &commonRect))
            {
                /* Calculate area of intersection */
                area = (long)(commonRect.bottom - commonRect.top) *
                       (long)(commonRect.right - commonRect.left);

                /* Keep track of largest area of intersection found so far */
                if (area > maxArea)
                {
                    maxArea = area;
                    baseGDevice = aGDevice;
                }
            }

            /* Go to the next GDevice in the GDevice list */
            aGDevice = GetNextDevice( aGDevice );
        }
    }
    else
        error = paramErr;

    /* If no screens intersect the bounds, baseDevice is NIL */
    if (baseGDevice != nil && error == noErr)
    {
        /* Normalize the bounds rectangle */
        normalBounds = *bounds;
        OffsetRect( &normalBounds, -normalBounds.left, -normalBounds.top);

        /* Create off-screen graphics environment w/ depth, clut of screen */
        basePixMap = (**baseGDevice).gdPMap;
        error = CreateOffScreen( &normalBounds, (**basePixMap).pixelSize,
                (**basePixMap).pmTable, retPort, retGDevice );
    }
    return error;
}

Finding the deepest screen that intersects an on-screen area is trivially easy because there's a Graphics Devices Manager routine that finds it called _GetMaxDevice which is documented on page 21-22 of the "Graphics Devices Manager" chapter of Inside Macintosh Volume VI. The rectangle in global coordinates of the screen area you're interested in is passed to _GetMaxDevice, and it returns a handle to the deepest screen that intersects that area, even if the area of intersection is as small as one pixel. If no screens intersect that area, then _GetMaxDevice returns NIL.

Finding the GDevice of the screen that has the maximum area of intersection with the screen area you're interested in isn't quite so easy because there's no single Graphics Devices Manager routine to find this GDevice; you have to search the GDevice list yourself. You can get a handle to the first GDevice in the list by calling _GetDeviceList, and you can get a handle to each successive GDevice by calling _GetNextDevice. _GetDeviceList is documented on pages 21-21 through 21-22 of the "Graphics Devices Manager" chapter of Inside Macintosh Volume VI, and _GetNextDevice is documented on page 21-22 of the same chapter. For each GDevice in the list, the area of intersection between the bounds and the gdRect of the GDevice is calculated. If the calculated area is the largest area of intersection found so far, then that area and the GDevice of that screen are remembered.

Once a winning GDevice has been chosen, either by being the deepest intersecting GDevice or the GDevice with the largest intersecting area, then CreateOffScreen routine is called with the pixel depth and color table of the PixMap of the GDevice, and the bounds rectangle normalized so that its top-left coordinate has the coordinates (0, 0). CreateOffScreen returns with the new off-screen graphics environment, and CreateScreenOffScreen returns this to the caller.

Choosing Your Off-Screen Memory

The CreateOffScreen routine in Listing 1 creates an off-screen graphics environment with its pixel image allocated as a nonrelocatable block in the application's heap. But this isn't the only way that the pixel image can be allocated. Pixel images can be big, and big blocks of nonrelocatable memory in your heap can be expensive in terms of performance, and they can cause a bad case of heap fragmentation. Why not put the pixel image in a relocatable block of memory instead? If there isn't much free memory in your heap and if MultiFinder or system software version 7.0 is running, there's memory that's not being used by any open applications, called temporary memory (formerly called MultiFinder temporary memory). Why not use this area of memory for the pixel image? Some people have NuBus[TM] cards with plenty of memory on them. Why not move the pixel image out of the heaps altogether and instead use NuBus memory for the pixel image? All of these things can be done with simple modifications to what's been discussed in this Note, and these modifications are discussed in the next few paragraphs.

How can pixel images be relocatable? After all, pixel images are referred to only by the baseAddr field of a PixMap, and the baseAddr is a pointer, not a handle. It's true that while QuickDraw is being used to draw into a graphics environment, the pixel image had better not move or else QuickDraw will start drawing over the area of memory that the pixel image used to be rather than where it is. But if QuickDraw isn't doing anything with the graphics environment, then it doesn't care what happens to the pixel image as long as the baseAddr points to it once QuickDraw starts drawing into the graphics environment. This implies a strategy: allocate the pixel image as a relocatable block and let it float in the heap; when QuickDraw is about to to draw into the graphics environment or to copy from it, lock the pixel image and copy its master pointer into the baseAddr field of the PixMap; when the drawing or copying is finished, unlock the pixel image. There are many ways to implement this, and Listing 9 shows a code fragment for one very simple method.

MPW Pascal Listing 9

      ...
      (* Allocate the pixel image; use long multiplication to avoid overflow*)
      offBaseAddr := NewHandle(LongInt(bytesPerRow) * (bounds^.bottom -
            bounds^.top));
      IF offBaseAddr <> NIL THEN
         BEGIN
            (* Initialize fields common to indexed and direct PixMaps *)
            aPixMap^^.baseAddr := Ptr(offBaseAddr); (* Reference the image *)
      ...


PROCEDURE LockOffScreen(
   offScreenPort: CGrafPtr {Ptr to off-screen CGrafPort}
   );

   VAR
      offImageHnd: Handle; {Handle to the off-screen pixel image}

BEGIN
   (* Get the saved handle to the off-screen pixel image *)
   offImageHnd := Handle(offScreenPort^.portPixMap^^.baseAddr);

   (* Lock the handle to the pixel image *)
   HLock(offImageHnd);

   (* Put pixel image master pointer into baseAddr so that QuickDraw can use it*)
   offScreenPort^.portPixMap^^.baseAddr := offImageHnd^;
END;


PROCEDURE UnlockOffScreen(
   offScreenPort: CGrafPtr {Ptr to off-screen port}
   );

   VAR
      offImagePtr: Ptr;    {Pointer to the off-screen pixel image}
      offImageHnd: Handle; {Handle to the off-screen pixel image}

BEGIN
   (* Get the handle to the off-screen pixel image *)
   offImagePtr := offScreenPort^.portPixMap^^.baseAddr;
   offImageHnd := RecoverHandle(offImagePtr);

   (* Unlock the handle *)
   HUnlock(offImageHnd);

   (* Save the handle back in the baseAddr field *)
   offScreenPort^.portPixMap^^.baseAddr := Ptr(offImageHnd);
END;

MPW C Listing 9

        ...
        /* Allocate the pixel image; use long multiplication to avoid overflow*/
        offBaseAddr = NewHandle( (unsigned long)bytesPerRow
                        * (bounds->bottom - bounds->top) );
        if (offBaseAddr != nil)
        {
            /* Initialize fields common to indexed and direct PixMaps */
            (**aPixMap).baseAddr = (Ptr)offBaseAddr; /* Reference the image*/
        ...


void LockOffScreen(
    CGrafPtr offScreenPort) /* Pointer to the off-screen CGrafPort */
{
    Handle offImageHnd; /* Handle to the off-screen pixel image */

    /* Get the saved handle to the off-screen pixel image */
    offImageHnd = (Handle)(**offScreenPort->portPixMap).baseAddr;

    /* Lock the handle to the pixel image */
    HLock( offImageHnd );

    /* Put pixel image master pointer into baseAddr so that QuickDraw can use it */
    (**offScreenPort->portPixMap).baseAddr = *offImageHnd;
}


void UnlockOffScreen(
    CGrafPtr offScreenPort) /* Pointer to the off-screen CGrafPort */
{
    Ptr    offImagePtr; /* Pointer to the off-screen pixel image */
    Handle offImageHnd; /* Handle to the off-screen pixel image */

    /* Get the handle to the off-screen pixel image */
    offImagePtr = (**offScreenPort->portPixMap).baseAddr;
    offImageHnd = RecoverHandle( offImagePtr );

    /* Unlock the handle */
    HUnlock( offImageHnd );

    /* Save the handle back in the baseAddr field */
    (**offScreenPort->portPixMap).baseAddr = (Ptr)offImageHnd;
}

Listing 9 starts with a code fragment from the SetUpPixMap routine that's modified so that it allocates a new handle for the off-screen pixel image instead of a new pointer. This handle is saved in the baseAddr field for now. When you're about to draw into the off-screen graphics environment or to copy from it, the LockOffScreen routine in Listing 9 should be called with a pointer to the off-screen graphics environment's CGrafPort as the parameter. It takes the handle to the pixel image from the baseAddr field of the off-screen graphics environment's PixMap and passes it to _HLock which makes sure the pixel image can't move in the heap. Then, the pixel image's handle is dereferenced to get the master pointer to the pixel image, and this master pointer is copied into the baseAddr field. Now, QuickDraw can draw into or copy from the off-screen graphics environment.

When you're finished drawing into the off-screen graphics environment, the pixel image should be unlocked, and the UnlockOffScreen routine in Listing 9 does this. The baseAddr field of the PixMap holds the pixel image's master pointer, so this is passed to _RecoverHandle to get the pixel image's handle. This handle is passed to _HUnlock to let the pixel image float in the heap again, and then this handle is saved in the baseAddr field.

One potentially useful addition to the LockOffScreen routine would be a call to _MoveHHi just before the call to _HLock. This helps reduce heap fragmentation while the pixel image is locked by moving it up as high in the heap as possible before locking it, allowing the other relocatable blocks to move without tripping over it. You have to be careful with _MoveHHi though because it not only moves the handle as high in the heap as possible, it moves other relocatable blocks out of the top of the heap to make room for the handle. This could involve moving huge amounts of memory, and it's not unusual for _MoveHHi to take several seconds to do this.

How do you make an off-screen graphics environment that uses temporary memory for the pixel image? Temporary memory is allocated as handles, so there's almost no difference between using temporary memory and using relocatable blocks in your own heap in the way that Listing 9 shows. All you have to do is replace the calls to _NewHandle, _HLock, and _HUnlock with calls to _TempNewHandle, _TempHLock, and _TempHUnlock. If temporary memory handles are real, then you don't even have to replace the _HLock and _HUnlock calls--they work properly with temporary memory handles that are real.You can tell whether temporary memory handles are real or not by calling _Gestalt with the gestaltOSAttr selector. If the gestaltRealTempMemory bit is set, then all temporary memory handles are real. See the sections "About Temporary Memory" and "Using Temporary Memory" of Inside Macintosh Volume VI, pages 28-33 through 28-40.

How do you make an off-screen graphics environment that stores the pixel image on a NuBus memory card? The Macintosh Memory Manager doesn't keep track of heaps on NuBus memory cards so it can't be used to allocate memory on those cards, but if applications can use that card's memory at will, then an application can set up the off-screen graphics environment with its pixel image in the NuBus card's memory simply by setting the address of the card's memory in the baseAddr field of the off-screen graphics environment's PixMap instead of allocating anything.

If your NuBus memory card doesn't require 32-bit addressing mode to access its memory, then setting the baseAddr to the address of the NuBus card's memory is all you have to do. Some NuBus memory cards require its memory to be accessed in 32-bit addressing mode. Without 32-Bit QuickDraw, these memory cards can't be used for storing the pixel image of an off-screen graphics environment because Color QuickDraw without 32-Bit QuickDraw always reads and writes pixel images in 24-bit addressing mode regardless of whether the pixel image is in main memory, on a NuBus video card, or on a NuBus memory card. With 32-Bit QuickDraw, Color QuickDraw automatically switches to 32-bit addressing mode before reading or writing a pixel image that's on a video card. It won't know to switch to 32-bit addressing mode if your off-screen graphics environment uses a pixel image on a NuBus memory card that's not a video card, but you can tell it to make this switch by setting bit 2 of the pmVersion field of the PixMap for the off-screen graphics environment. This is normally done by logically ORing the pmVersion field with the predefined constant baseAddr32. See "About 32-Bit Addressing" in Issue 6 of develop, page 36, for more details about how QuickDraw handles addressing modes.

Back to top

The GWorld Factor

In May 1989, 32-Bit QuickDraw was introduced as an extension to the system. While it had a lot of new features, the GWorld mechanism was the one that made the big news. GWorlds are off-screen graphics environments that you can have the system put together in one call. There's no need for routines like CreateOffScreen, SetUpPixMap, or CreateGDevice - all of the off-screen graphics environment is set up with _NewGWorld. You can change most of its characteristics with _UpdateGWorld, set the current off-screen graphics environment with _SetGWorld, and get rid of the off-screen graphics environment with _DisposeGWorld. All the GWorld routines are described in the "Graphics Devices Manager" chapter of Inside Macintosh Volume VI. As an example, Listing 10 shows the same routine as the ExerciseOffScreen routine that's shown in Listing 5, but Listing 10 uses GWorlds rather than the do-it-yourself routines that are defined in this Note.

MPW Pascal Listing 10

PROCEDURE ExerciseOffScreen;

   CONST
      kOffDepth  = 8;    {Number of bits per pixel in off-screen environment}
      rGrayClut  = 1600; {Resource ID of gray-scale clut}
      rColorClut = 1601; {Resource ID of full-color clut}

   VAR
      grayPort:    GWorldPtr;  {Graphics environment for gray off screen}
      colorPort:   GWorldPtr;  {Graphics environment for color off screen}
      savedPort:   GrafPtr;    {Pointer to the saved graphics environment}
      savedDevice: GDHandle;   {Handle to the saved color environment}
      offColors:   CTabHandle; {Colors for off-screen environments}
      offRect:     Rect;       {Rectangle of off-screen environments}
      circleRect:  Rect;       {Rectangles for circle-drawing}
      count:       Integer;    {Generic counter}
      aColor:      RGBColor;   {Color used for drawing off-screen}
      error:       OSErr;      {Error return from off-screen creation}

BEGIN
   (* Set up the rectangle for the off-screen graphics environments *)
   SetRect(offRect, 0, 0, 256, 256);

   (* Get the color table for the gray off-screen graphics environment *)
   offColors := GetCTable(rGrayClut);

   (* Create the gray off-screen graphics environment *)
   error := NewGWorld(grayPort, kOffDepth, offRect, offColors, NIL, []);

   IF error = noErr THEN
      BEGIN
         (* Get the color table for the color off-screen graphics environment*)
         offColors := GetCTable(rColorClut);

         (* Create the color off-screen graphics environment *)
         error := NewGWorld(colorPort, kOffDepth, offRect, offColors, NIL,[]);

         IF error = noErr THEN
            BEGIN
               (* Save the current graphics environment *)
               GetGWorld(savedPort, savedDevice);

               (* Set the current graphics environment to the gray one *)
               SetGWorld(grayPort, NIL);

               (* Draw gray-scale ramp into the gray off-screen environment*)
               FOR count := 0 TO 255 DO
                  BEGIN
                     aColor.red := count * 257;
                     aColor.green := aColor.red;
                     aColor.blue := aColor.green;
                     RGBForeColor(aColor);
                     MoveTo(0, count);
                     LineTo(255, count);
                  END;

               (* Copy gray ramp into color off-screen colorized with green*)
               SetGWorld(colorPort, NIL);
               aColor.red := $0000; aColor.green := $FFFF; aColor.blue :=$0000;
               RGBForeColor(aColor);
               CopyBits(GrafPtr(grayPort)^.portBits,
                     GrafPtr(colorPort)^.portBits,
                     grayPort^.portRect,
                     colorPort^.portRect,
                     srcCopy + ditherCopy, NIL);

               (* Draw red, green, and blue circles *)
               PenSize(8, 8);
               aColor.red := $FFFF; aColor.green := $0000; aColor.blue :=$0000;
               RGBForeColor(aColor);
               circleRect := colorPort^.portRect;
               FrameOval(circleRect);
               aColor.red := $0000; aColor.green := $FFFF; aColor.blue :=$0000;
               RGBForeColor(aColor);
               InsetRect(circleRect, 20, 20);
               FrameOval(circleRect);
               aColor.red := $0000; aColor.green := $0000; aColor.blue :=$FFFF;
               RGBForeColor(aColor);
               InsetRect(circleRect, 20, 20);
               FrameOval(circleRect);

               (* Copy the color off-screen environment to the current port*)
               SetGWorld(savedPort, savedDevice);
               CopyBits(GrafPtr(colorPort)^.portBits,
                     savedPort^.portBits,
                     colorPort^.portRect,
                     savedPort^.portRect,
                     srcCopy, NIL);

               (* Dispose of the off-screen graphics environments *)
               DisposeGWorld grayPort);
               DisposeGWorld(colorPort);
            END;
      END;
END;

MPW C Listing 10

#define kOffDepth  8    /* Number of bits per pixel in off-screen environment
*/
#define rGrayClut  1600 /* Resource ID of gray-scale clut */
#define rColorClut 1601 /* Resource ID of full-color clut */

void ExerciseOffScreen()
{
    GWorldPtr  grayPort;    /* Graphics environment for gray off screen */
    GWorldPtr  colorPort;   /* Graphics environment for color off screen */
    CGrafPtr   savedPort;   /* Pointer to the saved graphics environment */
    GDHandle   savedDevice; /* Handle to the saved color environment */
    CTabHandle offColors;   /* Colors for off-screen environments */
    Rect       offRect;     /* Rectangle of off-screen environments */
    Rect       circleRect;  /* Rectangles for circle-drawing */
    short      count;       /* Generic counter */
    RGBColor   aColor;      /* Color used for drawing off-screen */
    OSErr      error;       /* Error return from off-screen creation */

    /* Set up the rectangle for the off-screen graphics environments */
    SetRect( &offRect, 0, 0, 256, 256 );

    /* Get the color table for the gray off-screen graphics environment */
    offColors = GetCTable( rGrayClut );

    /* Create the gray off-screen graphics environment */
    error = NewGWorld( &grayPort, kOffDepth, &offRect, offColors, nil,0 );

    if (error == noErr)
    {
        /* Get the color table for the color off-screen graphics environment*/
        offColors = GetCTable( rColorClut );

        /* Create the color off-screen graphics environment */
        error = NewGWorld( &colorPort, kOffDepth, &offRect, offColors,nil, 0 );

        if (error == noErr)
        {
            /* Save the current graphics environment */
            GetGWorld( &savedPort, &savedDevice );

            /* Set the current graphics environment to the gray one */
            SetGWorld( grayPort, nil );

            /* Draw gray-scale ramp into the gray off-screen environment */
            for (count = 0; count < 256; count++)
            {
                aColor.red = aColor.green = aColor.blue = count * 257;
                RGBForeColor( &aColor );
                MoveTo( 0, count );
                LineTo( 255, count );
            }

            /* Copy gray ramp into color off-screen colorized with green */
            SetGWorld( colorPort, nil );
            aColor.red = 0x0000; aColor.green = 0xFFFF; aColor.blue = 0x0000;
            RGBForeColor( &aColor );
            CopyBits( &((GrafPtr)grayPort)->portBits,
                    &((GrafPtr)colorPort)->portBits,
                    &grayPort->portRect,
                    &colorPort->portRect,
                    srcCopy | ditherCopy, nil );

            /* Draw red, green, and blue circles */
            PenSize( 8, 8 );
            aColor.red = 0xFFFF; aColor.green = 0x0000; aColor.blue = 0x0000;
            RGBForeColor( &aColor );
            circleRect = colorPort->portRect;
            FrameOval( &circleRect );
            aColor.red = 0x0000; aColor.green = 0xFFFF; aColor.blue = 0x0000;
            RGBForeColor( &aColor );
            InsetRect( &circleRect, 20, 20 );
            FrameOval( &circleRect );
            aColor.red = 0x0000; aColor.green = 0x0000; aColor.blue = 0xFFFF;
            RGBForeColor( &aColor );
            InsetRect( &circleRect, 20, 20 );
            FrameOval( &circleRect );

            /* Copy the color off-screen environment to the current port */
            SetGWorld( savedPort, savedDevice );
            CopyBits( &((GrafPtr)colorPort)->portBits,
                    &((GrafPtr)savedPort)->portBits,
                    &colorPort->portRect,
                    &savedPort->portRect,
                    srcCopy, nil );

            /* Dispose of the off-screen graphics environments */
            DisposeGWorld( grayPort );
            DisposeGWorld( colorPort );
        }
    }
}

_NewGWorld creates an off-screen graphics environment by creating a CGrafPort, PixMap, and GDevice--the same structures that you normally put together when you make an off-screen graphics environment yourself. In this aspect, and in fact in most aspects, there's nothing magical about GWorlds. Do GWorlds make the CreateOffScreen, DisposeOffScreen, and their dependents useless? That depends on what your needs are. What follows are a few issues about off-screen drawing and how that determines whether you use your own routines, such as CreateOffScreen, to create and maintain off-screen graphics environments or whether you use GWorlds for the same purpose.

I Want the Best Performance!

As mentioned in the last paragraph, there's nothing magical about GWorlds in most aspects. In one major aspect, there certainly is: the version of Color QuickDraw that runs with the 8*24 GC video card's acceleration on knows about GWorlds and can cache their CGrafPort, PixMap, GDevice, inverse table, color table, and pixel image on the 8*24 GC card if there's enough memory on it. When this is done, QuickDraw operations on the GWorld can be much faster than they'd normally be because the image data can stay in the card's memory where the fast microprocessor is, and image data doesn't have to move across NuBus in transfer operations between the GWorld and the screen. Additionally, these operations are executed asynchronously which increases the overall speed of your programs. For details about how the 8*24 GC card and GC QuickDraw work, see Guillermo Ortiz's article, "Macintosh Display Card 8*24 GC: The Naked Truth," in Issue 5 of develop.

8*24 GC QuickDraw doesn't know about the off-screen graphics environments that you create, so it doesn't cache its structures. All QuickDraw commands that move image data between the off-screen graphics environment and the screen have to move the data across NuBus, and that slows down the operation in comparison to keeping all the image data on the card.

If you want the highest possible drawing and copying performance with the 8*24 GC card, you must use GWorlds for your off-screen graphics environments.

I Want to Use a NuBus Memory Card for My GWorld's Off-Screen Pixel Image

One common desire is to use a NuBus memory card to hold a pixel image. Because GWorlds are so easy to set up, and because GWorlds have all the same parts that you can make for an off-screen graphics environment, it's tempting to make a GWorld and then point the baseAddr of the GWorld's PixMap at the NuBus card's memory. But GWorlds are designed to be fairly atomic structures, so they can't be changed in this way. You can change a GWorld's dimensions, depth, and color table because there's a routine (_UpdateGWorld) that is designed to change these things, but you can't change the pixel image without risking future compatibility.

If you want to have an off-screen graphics environment use a NuBus video card to store the pixel image, you should set up your own off-screen graphics environment rather than use GWorlds. This is covered earlier in this Note in"Choosing Your Off-Screen Memory."

I Want My Program to Work on All System Software Releases

GWorlds have been around since 32-Bit QuickDraw was released (while system software version 6.0.3 was current). Until system software version 7.0, 32-Bit QuickDraw was an optional part of the system, so you aren't guaranteed use of GWorlds even under recent system software releases. Obviously, if GWorlds aren't available and your program still has to work with off-screen graphics environments, then there's no choice but to use your own routines for creating, maintaining, and disposing of off-screen graphics environments. What's usually done in these cases is to check via _Gestalt whether GWorlds are available or not. If they aren't, then you create your off-screen graphics environment with your own routines. If they are, then you can use GWorlds without having to take up memory with your code for creating off-screen graphics environments yourself.

 

Back to top

Summary

Reliable, understandable, and maintainable off-screen drawing routines means not taking short-cuts. The most common problems that people run into with off-screen drawing routines is the appearance of strange colors and the gradual degradation of reliability as the program does more off-screen drawing. Building an off-screen graphics environment out of a CGrafPort, GDevice, and PixMap or by using GWorlds, combined with an understanding of how Color QuickDraw uses off-screen graphics environments, helps get rid of these problems. Hopefully, this Note helps you understand these things so that you can get better programs out the door faster.

 

Back to top

References

Apple Computer, Inc., Inside Macintosh Volume I, Addison-Wesley, Reading, MA, 1985

Apple Computer, Inc., Inside Macintosh Volume V, Addison-Wesley, Reading, MA, 1988.

Apple Computer, Inc., Inside Macintosh Volume VI, Addison-Wesley, Reading, MA, 1991.

Knaster, S., Macintosh Programming Secrets, Addison-Wesley, Reading, MA, 1988.

Leak, B., "Realistic Color For Real-World Applications," develop, January 1990, 4-21.

Ortiz, G., "Braving Offscreen GWorlds," develop, January 1990, 28-40.

Ortiz, G., "Deaccelerated _CopyBits & 8*24 GC QuickDraw," Macintosh Technical Note #289, January 1991.

Ortiz, G., "Macintosh Display Card 8*24 GC: The Naked Truth," develop, July 1990, 332-347.

Othmer, K., "QuickDraw's CopyBits Procedure: Better Than Ever in System 7.0," develop, Spring 1991, 23-42.

Tanaka, F., "Of Time and Space and _CopyBits," Macintosh Technical Note #277, June 1990.

Zap, J., F. Tanaka, J. Friedlander, and G. Jernigan, "Drawing Into an Off-Screen Bitmap," Macintosh Technical Note #41, June 1990.

 

Back to top

Change History

01-October-1991

Created.

01-July-1992

A very embarrassing bug was found in CreateOffScreen and UpdateOffScreen. If you try to create a 16- or 32-bit off-screen graphics environment, you'll just get a paramErr. It won't do that now.


NuBus is a trademark of Texas Instruments.

Back to top

Downloadables

Acrobat gif

Acrobat version of this Note (216K).

Download