Clipping RLE Sprites

Table of Contents:
  1. Clipping? Why???
  2. It's all trivial
  3. The top... and the thing below it.
  4. The Left Side
  5. The Right Side
  6. DrawRLESpriteC in Assembler
  7. An example program


Clipping? Why???

Hello again, this time we will be discussing the small matter of clipping RLE sprites. One question that might come to mind is "Why clip RLE sprites?". This is an easy question to answer though. One problem with computers in general is that when you have a bunch of sprites running around on the screen, they never want to stay fully within the boundaries of the screen. If you have placed a border around the screen, you know that if a sprite gets off of the edge, it will ruin the border.

Wouldn't it be nice to be able to specify arbitrary rectangular clipping boundaries for your sprites (like you do with lines and points)? Unfortunately, it will not be as easy as it was to clip normal rectangular sprites, but it is not an insurmountable problem. In order to make this a quick process, we need to reevaluate the structures that we are using for our RLE sprites.


It's all Trivial

Assuming that we are going to use the same framework to clip sprites as to clip lines (SetClipBoundary(), Screen.Clip.... etc), we can start building a version of the RLE sprite drawer that draws sprites clipped to the window. We will start by determining whether or not we can trivially accept or reject the sprite. It works something like this:

PROCEDURE DrawRLESpriteC(Sprite : SpriteType; X, Y : INTEGER);
BEGIN
  IF SpriteAllOutSide THEN
    EXIT
  ELSE IF SpriteAllInside THEN 
  BEGIN
    DrawRLESprite(Sprite, X, Y);
    EXIT;
  END;

  DrawItTheHardWay;
END;
To flesh it out more, we'll put the comparisons in:

PROCEDURE DrawRLESpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR Code : BYTE;
BEGIN
  IF (X > Screen.Clip.X2) OR (Y > Screen.Clip.Y2) OR
     (X+Sprite.Width  < Screen.Clip.X1) OR
     (Y+Sprite.Height < Screen.Clip.Y1) THEN
    EXIT;                                 { Trivially Reject }

  Code := 0;
  IF (X < Screen.Clip.X1) THEN Code := 1;
  IF (Y < Screen.Clip.Y1) THEN Code := Code OR 2;
  IF (X+Sprite.Width  > Screen.Clip.X2) THEN Code := Code OR 4;
  IF (Y+Sprite.Height > Screen.Clip.Y2) THEN Code := Code OR 8;

  IF Code = 0 THEN                        { Trivially Accept }
  BEGIN
    DrawRLESprite(Sprite, X, Y);
    EXIT;
  END;
 { DrawItTheHardWay; }
END;
Here you can see that I put in the little "Code" thing from the line clipping code. This helps classify which parts of the sprite need to be clipped. When we finally get ready to draw this, we won't have to redo all of the comparisons we have already done. It turns out that sprite clipping is actually easier to do than line clipping because you don't have to worry about two different points.


The top... and the thing below it.

Let's start with the Plain ol' Pascal version of the routine and start adding clipping capabilities to it. Well, here's the one that only has trivial rejection:

PROCEDURE DrawRLESpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Code : BYTE;
  I, J : INTEGER;
  ScreenPtr,
  SpritePtr : WORD;
  Count  : BYTE;
BEGIN
  IF (X > Screen.Clip.X2) OR (Y > Screen.Clip.Y2) OR
     (X+Sprite.Width  < Screen.Clip.X1) OR
     (Y+Sprite.Height < Screen.Clip.Y1) THEN
    EXIT;                                 { Trivially Reject }

  Code := 0;
  IF (X < Screen.Clip.X1) THEN Code := 1;
  IF (Y < Screen.Clip.Y1) THEN Code := Code OR 2;
  IF (X+Sprite.Width  > Screen.Clip.X2) THEN Code := Code OR 4;
  IF (Y+Sprite.Height > Screen.Clip.Y2) THEN Code := Code OR 8;

  IF Code = 0 THEN                        { Trivially Accept }
  BEGIN
    DrawRLESprite(Sprite, X, Y);
    EXIT;
  END;

  SpritePtr := 0;
  FOR I := 0 TO Sprite.Height-1 DO
  BEGIN
    J := 0;
    ScreenPtr := (Y+I)*Screen.Width+X;
    WHILE J < Sprite.Width DO
    BEGIN
      Count := Sprite.Data^[SpritePtr];
      INC(SpritePtr);
      IF (Count AND 1) = 0 THEN                 { Transparant run }
      BEGIN
        ScreenPtr := ScreenPtr + (Count SHR 1);
        J := J + (Count SHR 1);
      END
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        WHILE Count > 0 DO
        BEGIN
          Screen.Buffer^[ScreenPtr] := Sprite.Data^[SpritePtr];
          INC(ScreenPtr); INC(J);
          INC(SpritePtr); DEC(Count);
        END;
      END;
    END;
  END;
END;
Now if we just make a local copy of the Sprite.Height variable, we can draw a different number of lines than the sprite contains. If the top or the bottom of the sprite is clipped, then this will allow the main body of the routine to run normally. This simply skips over the lines on the top, and stops drawing before we get to the lines on the bottom. To do this, we modify the following lines:

  IF Code = 0 THEN                        { Trivially Accept }
  BEGIN
    DrawRLESprite(Sprite, X, Y);
    EXIT;
  END;

  IF (Code AND 8) = 8 THEN                { Clip the bottom  }
    Height := Screen.Clip.Y2-Y+1;

  SpritePtr := 0;
  FOR I := 0 TO Height-1 DO
  BEGIN
    J := 0;
So what this does, is that if the number of lines in the sprite would exceed the number of lines that we can display in the current clipping area, then we simple decrease the number of lines to display. This is simple enough, but how do we clip the top of the sprite? We have two alternatives here:

  1. We could step through all of the spans in the sprite until we get to the desired scan line in the sprite. This could be hundreds of spans, or it could just be a few. The advantage of this method over the second is that it uses less memory. The disadvantage is that it is much slower.
  2. We could keep a list of the starting positions of each scanline and jump directly to the line that we need. The advantage to this method is that we can jump directly to where we are going, instead of stepping through all of the spans. The disadvantage is that it uses slightly more memory, and some of our old routines need to be updated. In my opinion, this is the best method for our applications.

Okay then, how do we go about implementing option #2? First of all, we have to modify our sprite conversion routine that converts normal sprites to RLE sprites. The list of pointers to all of the scan lines should be at the beginning of the sprite for easy reference. This should be an array of words that contains the offset into the sprite data of the scan lines. It turns out that it isn't a very big deal to update the sprite creation routine or to update the draw without clipping routine. Here is the new sprite conversion routine:

PROCEDURE ConvertSpriteToRLE(VAR Sprite : SpriteType);
VAR
  SpriteSrc, SpriteDest : WORD;
  NewSprite : SpriteType;
  X, Y, I   : INTEGER;
  RunCount  : INTEGER;
  RunType   : BOOLEAN;
BEGIN
  IF Sprite.SType <> STStandard THEN EXIT;
  NewSprite.Width   := Sprite.Width;
  NewSprite.Height  := Sprite.Height;
  NewSprite.DataLen := Sprite.Width*Sprite.Height*3 DIV 2 + Sprite.Height*2;
  NewSprite.SType   := STRLE;
  GETMEM(NewSprite.Data, NewSprite.DataLen);

  SpriteSrc := 0; SpriteDest := Sprite.Height*2;  { Skip past the list }
  FOR Y := 1 TO Sprite.Height DO
  BEGIN
    NewSprite.Data^[(Y-1)*2  ] := SpriteDest AND 255; { Update the list      }
    NewSprite.Data^[(Y-1)*2+1] := SpriteDest DIV 256; { They are split bytes }
    RunCount := 0;
    RunType := Sprite.Data^[SpriteSrc] <> 0;    { FALSE = Transparent }
    FOR X := 1 TO Sprite.Width DO
    BEGIN
      IF RunType <> (Sprite.Data^[SpriteSrc] <> 0) THEN
      BEGIN
        NewSprite.Data^[SpriteDest] :=
          (RunCount SHL 1) + (BYTE(RunType) AND 1);
        INC(SpriteDest);
        IF RunType = TRUE THEN
          FOR I := SpriteSrc-RunCount TO SpriteSrc-1 DO
          BEGIN
            NewSprite.Data^[SpriteDest] := Sprite.Data^[I];
            INC(SpriteDest);
          END;
        RunCount := 0;
        RunType := Sprite.Data^[SpriteSrc] <> 0;
      END;
      INC(RunCount);
      INC(SpriteSrc);
    END;
    IF RunCount > 0 THEN
    BEGIN
      NewSprite.Data^[SpriteDest] :=
        (RunCount SHL 1) + (BYTE(RunType) AND 1);
      INC(SpriteDest);
      IF RunType = TRUE THEN
        FOR I := SpriteSrc-RunCount TO SpriteSrc-1 DO
        BEGIN
          NewSprite.Data^[SpriteDest] := Sprite.Data^[I];
          INC(SpriteDest);
        END;
      RunCount := 0;
      RunType := Sprite.Data^[SpriteSrc] <> 0;
    END;
  END;
  KillSprite(Sprite);
  Sprite := NewSprite;
  Sprite.DataLen := SpriteDest;             { Resize the sprite data to the }
  GETMEM(Sprite.Data, Sprite.DataLen);      { exact amount needed           }
  Move(NewSprite.Data^, Sprite.Data^, Sprite.DataLen); { Copy the data      }
  KillSprite(NewSprite);
END;
As you can see, the only thing that changes is that for each scanline, an index is generated, and that an extra chunk of memory is allocated at the beginning. The only changes to the DrawRLESprite routine that are neccessary is the addition of two add statements. Since the changes are so small, you can look them up in the source code for the unit. So, how do we add top clipping to our DrawRLESpriteC routine? With the addition of the above data and code, it becomes simple.

All that we have to add to our routine is the following code. It detects whether or not the top of the sprite needs to be clipped. If it does, it reduces the number of lines to draw in the sprite, and it updates the starting line. Finally, the routine sets the top of the clipping window as the starting value for the Y coordinate.

  IF (Code AND 8) = 8 THEN                { Clip the bottom  }
    Height := Screen.Clip.Y2-Y+1;

  StartLine := 0;
  IF (Code AND 2) = 2 THEN                { Clip the top     }
  BEGIN
    StartLine := Screen.Clip.Y1-Y;        { Update the starting line }
    Height    := Height-StartLine;        { Reduce the height        }
    Y := Screen.Clip.Y1;                  { Set the new Y value      }
  END;

  { This should simply be SpritePtr := Word(Sprite.Data^[StartLine*2]),
    but TP doesn't like that.  Converting it to assembly code with clean
    this up a lot.                                                          }
  SpritePtr := Sprite.Data^[StartLine*2]+Sprite.Data^[StartLine*2+1]*256;

  FOR I := 0 TO Height-1 DO
  BEGIN
Wow! Now the top and bottoms of our sprites can be clipped. Now we just have to go onto the left and right sides!


Clipping the left side

Okay, now we have two sides down and only two to go. The left side is the most cumbersome to clip though, as there is no easy was to skip the pixels that we don't want to draw. In this case, we have to just step over each of the runs until we get to the run that we wish to draw. Most likely, the run will be split by the left clipping boundary though, so we must break it into two pieces (not in the sprite, just in local variables) before we draw the second half.

So now, how do we go about this? First of all, I'm going to rearrange the routine a little bit. Before, the inner loop looked like this:

    WHILE J < Sprite.Width DO
    BEGIN
**    Count := Sprite.Data^[SpritePtr];
**    INC(SpritePtr);
      IF (Count AND 1) = 0 THEN                 { Transparant run }
      BEGIN
        ScreenPtr := ScreenPtr + (Count SHR 1);
        J := J + (Count SHR 1);
      END
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        WHILE Count > 0 DO
        BEGIN
          Screen.Buffer^[ScreenPtr] := Sprite.Data^[SpritePtr];
          INC(ScreenPtr); INC(J);
          INC(SpritePtr); DEC(Count);
        END;
      END;
    END;
What I want to do is move the two **'ed lines around a little bit, so that they look like this:

**  Count := Sprite.Data^[SpritePtr];
**  INC(SpritePtr);
    WHILE J < Width DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
      ...
      ELSE                                      { Block of pixels }
      ...
**    Count := Sprite.Data^[SpritePtr];
**    INC(SpritePtr);
    END;
Why would I do this? It turns out that if we do do this, we can modify the first run of the sprite that we want to draw. This means that if we need to only draw the second half of it, we can modify Count and SpritePtr so that only that part gets drawn. So now that we have this, we need to generate the code that skips over the spans if the are entirely to the left of the left clip area. This is a fairly trivial piece of code...

    WHILE J+(Count SHR 1) <= LeftClip DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
        Count := Count SHR 1
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        SpritePtr := SpritePtr + Count;
      END;
      J := J + Count;
      Count := Sprite.Data^[SpritePtr];
      INC(SpritePtr);
    END;
First, this code starts stepping through the spans until it gets to the one that crosses over the Left Clipping boundary. For each span of pixels that is completely to the left of the left clip boundary, the variable J is kept current with the X position within the sprite (running from 0 to Width-1). Also, the pointer within the sprite has to be current so that we know where the next span is and where to get the pixel data if it is a block of data pixels.

When we get dumped out of this loop, SpritePtr points the the span that either is completely to the right of the left clip line, or is split by the left clip line. From here, we must go on taking care of the possible case where we need to draw the second half of a split segment. This is the last part of the puzzle that we have to put together to get the left clipping to work. Here's the code:

    IF J+(Count SHR 1) > LeftClip THEN          { If a run is split...    }
      IF (Count AND 1) = 0 THEN
      BEGIN                                     { Transparent run         }
        Count := Count SHR 1;
        Count := Count+J-LeftClip;              { Update count to reflect }
        Count := Count SHL 1;                   { Only the end of the run }
      END
      ELSE
      BEGIN                                     { Block of pixels         }
        Count := Count SHR 1;
        SpritePtr := SpritePtr + LeftClip-J;    { Update Count and the    }
        Count := Count+J-LeftClip;              { SpritePtr variables     }
        Count := Count SHL 1+1;
      END;

    J := LeftClip;
    WHILE J < Width DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
       ...
As you can see above, this code takes the run (if it crosses the left clip boundary. It is possible that it starts on the left clip boundary and requires no clipping at all) and adjusts the Count variable so that the main loop can process it correctly. It also adjust the SpritePtr variable in the case that the run is a data block. This ensures that only pixels inside of the clipping area get drawn. This completes the code to get the routines to clip for the left boundary. Here is the clipping RLE sprite routine so far:

PROCEDURE DrawRLESpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Code           : BYTE;
  I, J           : INTEGER;
  LeftClip,
  Width, Height,
  ScreenPtr,
  StartLine,
  SpritePtr      : WORD;
  Count          : BYTE;
BEGIN
  Height := Sprite.Height;
  Width := Sprite.Width;
  IF (X > Screen.Clip.X2) OR (Y > Screen.Clip.Y2) OR
     (X+Width  < Screen.Clip.X1) OR
     (Y+Height < Screen.Clip.Y1) THEN
    EXIT;                                 { Trivially Reject }

  Code := 0;
  IF (X < Screen.Clip.X1) THEN Code := 1;
  IF (Y < Screen.Clip.Y1) THEN Code := Code OR 2;
  IF (X+Width  > Screen.Clip.X2) THEN Code := Code OR 4;
  IF (Y+Height > Screen.Clip.Y2) THEN Code := Code OR 8;

  IF Code = 0 THEN                        { Trivially Accept }
  BEGIN
    DrawRLESprite(Sprite, X, Y);
    EXIT;
  END;

  IF (Code AND 8) = 8 THEN                { Clip the bottom  }
    Height := Screen.Clip.Y2-Y+1;

  StartLine := 0;
  IF (Code AND 2) = 2 THEN                { Clip the top     }
  BEGIN
    StartLine := Screen.Clip.Y1-Y;        { Update the starting line }
    Height    := Height-StartLine;        { Reduce the height        }
    Y := Screen.Clip.Y1;                  { Set the new Y value      }
  END;

  LeftClip := 0;
  IF (Code AND 1) = 1 THEN
    LeftClip := Screen.Clip.X1-X;

  FOR I := 0 TO Height-1 DO
  BEGIN
    J := 0;
    SpritePtr := Sprite.Data^[(StartLine+I)*2]+
                 Sprite.Data^[(StartLine+I)*2+1] SHL 8;
    ScreenPtr := (Y+I)*Screen.Width+X+LeftClip;

    Count := Sprite.Data^[SpritePtr];
    INC(SpritePtr);

    WHILE J+(Count SHR 1) <= LeftClip DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
        Count := Count SHR 1
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        SpritePtr := SpritePtr + Count;
      END;
      J := J + Count;
      Count := Sprite.Data^[SpritePtr];
      INC(SpritePtr);
    END;

    IF J+(Count SHR 1) > LeftClip THEN          { If a run is split...    }
      IF (Count AND 1) = 0 THEN
      BEGIN                                     { Transparent run         }
        Count := Count SHR 1;
        Count := Count+J-LeftClip;              { Update count to reflect }
        Count := Count SHL 1;                   { Only the end of the run }
      END
      ELSE
      BEGIN                                     { Block of pixels         }
        Count := Count SHR 1;
        SpritePtr := SpritePtr + LeftClip-J;    { Update Count and the    }
        Count := Count+J-LeftClip;              { SpritePtr variables     }
        Count := Count SHL 1+1;
      END;

    J := LeftClip;
    WHILE J < Width DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
      BEGIN
        Count := Count SHR 1;
        ScreenPtr := ScreenPtr + Count;
        J := J + Count;
      END
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        WHILE Count > 0 DO
        BEGIN
          Screen.Buffer^[ScreenPtr] := Sprite.Data^[SpritePtr];
          INC(ScreenPtr);
          INC(SpritePtr);
          DEC(Count);
          INC(J);
        END;
      END;
      Count := Sprite.Data^[SpritePtr];
      INC(SpritePtr);
    END;
  END;
END;
All we have to do now is get the right side to work correctly... :)


The Right Side

Although our code is starting to get very large, we are almost done. Even though this code is starting to get ugly, it will convert nicely to assembly language. That is one of the nice things about assembler... It can make code that is ugly and long look kinda nice and logical. For example, the silly SHR stuff that we have to do here virtually dissapears when it gets assemblerized. Anyhoo, on with the show!

To get it to clip the right side, we only have to add one chunk of code, and make one small modification. The first small modification is to change the main while loop that steps over each visible span:

        Count := Count+J-LeftClip;              { SpritePtr variables     }
        Count := Count SHL 1+1;
      END;

    J := LeftClip;
**  WHILE J+(Count SHR 1) < RightClip DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
      BEGIN
        Count := Count SHR 1;
You can see that the **ed code has been changed. It now goes until a span will overflow the right side of the clipping area (Which is equal to the width if it doesn't require right clipping). This means that the loop will terminate with a span that needs to be clipped. Here is the code that draws the selected amount of the sprite:

    IF J+(Count SHR 1) >= RightClip THEN         { If a run is split...    }
      IF (Count AND 1) = 1 THEN                  { Ignore transparent runs }
      BEGIN                                      { Clip blocks of pixels   }
        Count := RightClip-J-1;                  { Just move what's left   }
        WHILE Count > 0 DO                       { Do it!                  }
        BEGIN
          Screen.Buffer^[ScreenPtr] := Sprite.Data^[SpritePtr];
          INC(ScreenPtr); INC(SpritePtr);
          DEC(Count); INC(J);
        END;
      END;
All of this code will condense into a couple of cmp instructions and a movsb instruction in assembly language. Until then though, here is the complete routine:

PROCEDURE DrawRLESpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Code           : BYTE;
  I, J           : INTEGER;
  LeftClip, RightClip,
  Height,
  ScreenPtr,
  StartLine,
  SpritePtr      : WORD;
  Count          : BYTE;
BEGIN
  Height := Sprite.Height;
  RightClip := Sprite.Width;
  IF (X > Screen.Clip.X2) OR (Y > Screen.Clip.Y2) OR
     (X+RightClip < Screen.Clip.X1) OR
     (Y+Height    < Screen.Clip.Y1) THEN
    EXIT;                                 { Trivially Reject }

  Code := 0;
  IF (X < Screen.Clip.X1) THEN Code := 1;
  IF (Y < Screen.Clip.Y1) THEN Code := Code OR 2;
  IF (X+RightClip > Screen.Clip.X2) THEN Code := Code OR 4;
  IF (Y+Height    > Screen.Clip.Y2) THEN Code := Code OR 8;

  IF Code = 0 THEN                        { Trivially Accept }
  BEGIN
    DrawRLESprite(Sprite, X, Y);
    EXIT;
  END;

  IF (Code AND 8) = 8 THEN                { Clip the bottom  }
    Height := Screen.Clip.Y2-Y+1;

  IF (Code AND 4) = 4 THEN
    RightClip := Screen.Clip.X2-X+2;

  StartLine := 0;
  IF (Code AND 2) = 2 THEN                { Clip the top     }
  BEGIN
    StartLine := Screen.Clip.Y1-Y;        { Update the starting line }
    Height    := Height-StartLine;        { Reduce the height        }
    Y := Screen.Clip.Y1;                  { Set the new Y value      }
  END;

  LeftClip := 0;
  IF (Code AND 1) = 1 THEN
    LeftClip := Screen.Clip.X1-X;

  FOR I := 0 TO Height-1 DO
  BEGIN
    J := 0;
    SpritePtr := Sprite.Data^[(StartLine+I)*2]+
                 Sprite.Data^[(StartLine+I)*2+1] SHL 8;
    ScreenPtr := (Y+I)*Screen.Width+X+LeftClip;

    Count := Sprite.Data^[SpritePtr];
    INC(SpritePtr);

    WHILE J+(Count SHR 1) <= LeftClip DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
        Count := Count SHR 1
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        SpritePtr := SpritePtr + Count;
      END;
      J := J + Count;
      Count := Sprite.Data^[SpritePtr];
      INC(SpritePtr);
    END;

    IF J+(Count SHR 1) > LeftClip THEN          { If a run is split...    }
      IF (Count AND 1) = 0 THEN
      BEGIN                                     { Transparent run         }
        Count := Count SHR 1;
        Count := Count+J-LeftClip;              { Update count to reflect }
        Count := Count SHL 1;                   { Only the end of the run }
      END
      ELSE
      BEGIN                                     { Block of pixels         }
        Count := Count SHR 1;
        SpritePtr := SpritePtr + LeftClip-J;    { Update Count and the    }
        Count := Count+J-LeftClip;              { SpritePtr variables     }
        Count := Count SHL 1+1;
      END;

    J := LeftClip;
    WHILE J+(Count SHR 1) < RightClip DO
    BEGIN
      IF (Count AND 1) = 0 THEN                 { Transparant run }
      BEGIN
        Count := Count SHR 1;
        ScreenPtr := ScreenPtr + Count;
        J := J + Count;
      END
      ELSE                                      { Block of pixels }
      BEGIN
        Count := Count SHR 1;
        J := J + Count;
        WHILE Count > 0 DO
        BEGIN
          Screen.Buffer^[ScreenPtr] := Sprite.Data^[SpritePtr];
          INC(ScreenPtr);
          INC(SpritePtr);
          DEC(Count);
        END;
      END;
      Count := Sprite.Data^[SpritePtr];
      INC(SpritePtr);
    END;

    IF J+(Count SHR 1) >= RightClip THEN         { If a run is split...    }
      IF (Count AND 1) = 1 THEN                  { Ignore transparent runs }
      BEGIN                                      { Clip block of pixels    }
        Count := RightClip-J-1;                  { Just move what's left   }
        WHILE Count > 0 DO                       { Do it!                  }
        BEGIN
          Screen.Buffer^[ScreenPtr] := Sprite.Data^[SpritePtr];
          INC(ScreenPtr);
          INC(SpritePtr);
          DEC(Count);
        END;
      END;
  END;
END;


DrawRLESpriteC in Assembler

Because of the length of the code, I don't really want to explain all of the conversion process. Instead, I have documented it with the pascal source code that it came from. I hope that it is clear... but it is definately fast. Don't let the length fool you, it is nearly as fast as non-clipped code...

PROCEDURE DrawRLESpriteC(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
VAR
  I              : INTEGER;
  LeftClip, RightClip,
  Height,
  ScreenOffs,
  StartLine,
  SavedSI,
  ScreenWidth    : WORD;
ASM
  mov AX, Screen.Width     { Copy this into a local variable so that }
  mov ScreenWidth, AX      {  it will be available after we trash DS }

  les SI, Sprite
  mov CX, ES:[SI+SpriteType.Width]   { RightClip := Sprite.Width }
  mov RightClip, CX
  mov DX, ES:[SI+SpriteType.Height]  { Height := Sprite.Height   }
  mov Height, DX

  mov AX, X
  mov BX, Y
  cmp AX, Screen.Clip.X2
  jg @TriviallyReject
  cmp BX, Screen.Clip.Y2
  jg @TriviallyReject

  add CX, AX
  add DX, BX
  cmp CX, Screen.Clip.X1
  jl @TriviallyReject
  cmp DX, Screen.Clip.Y1
  jge @DontTriviallyReject
@TriviallyReject:
  jmp @GetOutOfHere                                    { Trivially Reject }
@DontTriviallyReject:

  mov DI, FALSE                  { Code := FALSE                          }
  cmp DX, Screen.Clip.Y2         { IF (Y+Height > Screen.Clip.Y2) THEN    }
  jle @DontClipBottom                 { Clip the bottom          }
  mov DX, Screen.Clip.Y2
  sub DX, Y
  inc DX
  mov Height, DX                 {   Height := Screen.Clip.Y2-Y+1         }
  mov DI, TRUE                   {   Code := TRUE                         }
@DontClipBottom:                 { END                                    }

  cmp CX, Screen.Clip.X2         { IF (X+RightClip > Screen.Clip.X2) THEN }
  jle @DontClipRight                  { Clip the right           }
  mov CX, Screen.Clip.X2
  sub CX, X
  inc CX
  mov RightClip, CX              {   RightClip := Screen.Clip.X2-X+2      }
  mov DI, TRUE                   {   Code := TRUE                         }
@DontClipRight:                  { END                                    }

  mov CX, 0                      { StartLine := 0                         }
  cmp BX, Screen.Clip.Y1         { IF (Y < Screen.Clip.Y1) THEN           }
  jge @DontClipTop                    { Clip the top             }
  mov AX, Screen.Clip.Y1
  sub AX, BX                     {   StartLine := Screen.Clip.Y1-Y        }
  mov CX, AX                          { Update the starting line }
  mov AX, CX                     {   Height    := Height-StartLine        }
  sub Height, AX                      { Reduce the height        }

  mov AX, Screen.Clip.Y1         {   Y := Screen.Clip.Y1                  }
  mov Y, AX                           { Set the new Y value      }
  mov DI, TRUE                   {   Code := TRUE                         }
@DontClipTop:                    { END                                    }
  mov StartLine, CX

  mov CX, 0                      { LeftClip := 0                          }
  mov AX, X
  cmp AX, Screen.Clip.X1         { IF (X < Screen.Clip.X1) THEN           }
  jge @DontClipLeft                   { Clip the left            }
  mov CX, Screen.Clip.X1
  sub CX, AX                     {   LeftClip := Screen.Clip.X1-X         }
  mov DI, TRUE                   {   Code := TRUE                         }
@DontClipLeft:                   { END                                    }
  mov LeftClip, CX

  or DI, DI                      { IF Code = FALSE THEN                   }
  jnz @DontTriviallyAccept            { Trivially Accept         }

  push WORD [Sprite+2]
  push WORD [Sprite]
  push X
  push Y
  call DrawRLESprite                  {   DrawRLESprite(Sprite, X, Y)          }
  jmp @GetOutOfHere
@DontTriviallyAccept:            { END                                    }

  les DI, Screen.Buffer
  lds SI, Sprite                  { DS:SI now points to Sprite }

  mov BX, Y
  add BX, BX
  mov AX, DS:[BX+OFFSET Screen.YTable]
  add AX, X
  add AX, LeftClip
  add AX, DI
  mov ScreenOffs, AX

  lds SI, DS:[SI+SpriteType.Data] { DS:SI now points to Sprite.Data }

  shl StartLine, 1
  mov SavedSI, SI
  add StartLine, SI
  mov I, 0
@ILoop:                        { FOR I := 0 TO Height-1 DO                 }
  mov BX, StartLine
  add StartLine, 2
  mov SI, DS:[BX]              {   SpritePtr := Sprite.Data^[(StartLine+I)*2]+        }
  add SI, SavedSI              {                Sprite.Data^[(StartLine+I)*2+1] SHL 8 }

  mov AX, ScreenWidth          {   ScreenPtr := (Y+I)*ScreenWidth+X+LeftClip }
  xor BX, BX                   {   J := 0                                  }
  mov DI, ScreenOffs
  add ScreenOffs, AX

  mov CH, 0
  mov CL, DS:[SI]              {   Count := Sprite.Data^[SpritePtr]        }

  inc SI                       {   INC(SpritePtr)                          }
  xor DX, DX
  shr CL, 1
  rcl DX, 1

  mov AX, BX
@ThisWhileLoop:
  add AX, CX

  cmp AX, LeftClip             {   WHILE J+(Count SHR 1) <= LeftClip DO    }
  ja @SkipThisWhileLoop        {   BEGIN                                   }

  or DX, DX                    {     IF (Count AND 1) = 1 THEN             }
  jz @DontUpdateSpritePtr            { Block of pixels }
  add SI, CX                   {        SpritePtr := SpritePtr + Count     }
@DontUpdateSpritePtr:
  mov BX, AX                   {     J := J + Count                        }
  mov CL, DS:[SI]              {     Count := Sprite.Data^[SpritePtr]      }
  inc SI                       {     INC(SpritePtr)                        }

  xor DL, DL
  shr CL, 1
  rcl DX, 1
  jmp @ThisWhileLoop           {    END                                    }
@SkipThisWhileLoop:

{ cmp AX, LeftClip             {    IF J+(Count SHR 1) > LeftClip THEN     }
  jbe @SkipSplitRun                 { If a run is split...    }

  or DX, DX                    {      IF (Count AND 1) = 1 THEN            }
  jz @SkipUpdateSpritePtr           { Block of pixels }
  mov AX, LeftClip
  sub AX, BX
  add SI, AX                   {        SpritePtr := SpritePtr + LeftClip-J}

@SkipUpdateSpritePtr:
  shr DX, 1
  rcl CX, 1         { Reassemble Count to include carry bit }

  mov AX, BX
  sub AX, LeftClip
  add AX, AX
  add CX, AX                   {       Count := Count+((J-LeftClip) SHL 1) }
@SkipSplitRun:

  mov BX, LeftClip             {    J := LeftClip                          }

  xor DX, DX
  shr CL, 1
  rcl DX, 1

  mov AX, BX
@MainWhileLoop:
  add AX, CX

  cmp AX, RightClip            {   WHILE J+(Count SHR 1) <= RightClip DO   }
  ja @SkipMainWhileLoop        {   BEGIN                                   }

  or DL, DL                    {     IF (Count AND 1) = 0 THEN             }
  jnz @PixelRun                      { Transparant run }
  add DI, CX                   {        ScreenPtr := ScreenPtr + Count     }
  mov CL, 0 { Don't execute the rep movsb }

@PixelRun:                     {     ELSE                { Block of pixels }
  rep movsb                    {       WHILE Count > 0 DO...               }
  mov BX, AX

  mov CL, DS:[SI]              { Count := Sprite.Data^[SpritePtr]          }
  inc SI                       {   INC(SpritePtr)                          }

  xor DX, DX
  shr CL, 1
  rcl DX, 1
  jmp @MainWhileLoop           {    END                                    }
@SkipMainWhileLoop:

{ cmp AX, RightClip            {   IF J+(Count SHR 1) > RightClip THEN     }
  jbe @SkipRightSplitRun              { If a run is split...    }
  or DX, DX                    {     IF (Count AND 1) = 1 THEN             }
  jz @SkipRightSplitRun               { Ignore transparent runs }
                               {     BEGIN          { Clip block of pixels }
  mov CX, RightClip                   { Just move what's left   }
  sub CX, BX                   {       Count := RightClip-J                }
  rep movsb                    {       WHILE Count > 0 DO...               }

@SkipRightSplitRun:

  inc I
  mov AX, Height
  cmp I, AX
  jb @ILoop

  mov AX, SEG @Data
  mov DS, AX
@GetOutOfHere:
END;


Example Program

Okay, here we go! This example program show clipping sprites in action. It is a cool little program that animates a sprite moving around the screen and times it. Here are the results that I get:

        Size:   63   31
 Clipping on:  3468 8301
Clipping off:  2742 8121
As you can see, in my case, it was actually faster to draw the sprites with clipping than without. I think that this is because my processor is much faster than video memory, so avoiding writes to video memory is worth the extra work. If you don't want to compile this program yourself (because of the extra units), you can grab it
here. All of the options can be specified as command line parameters... Anyways, here we go:

-----------------] Example Starts here [-----------------
PROGRAM RLESpriteClippingExample;
USES CRT, GraphPro, Sprite, FPS;

CONST
  ClipX1 = 50;
  ClipY1 = 40;
  ClipX2 = 270;
  ClipY2 = 160;
VAR
  Size,
  I : INTEGER;
  Clip : BOOLEAN;
  Font : FontType;
  Spr  : SpriteType;
  X, Y : INTEGER;
  Radius, Theta : REAL;
  Flag : STRING;
BEGIN
  Size := 63;
  Clip := TRUE;
  Randomize;

  FOR X := 1 TO ParamCount DO
  BEGIN
    Flag := ParamStr(X);
    FOR Y := 1 TO Length(Flag) DO
      Flag[Y] := Upcase(Flag[Y]);

    IF Flag = '-C' THEN
      Clip := FALSE
    ELSE
    BEGIN
      Val(Flag, I, Y);
      IF Y = 0 THEN Size := I;
      IF Size < 19 THEN Size := 19;
    END;
  END;


  InitGraph;                          { Initialize the graphics    }
  LoadBIOSFont(Font, 16);             { Get a 16 point font        }

  ClrScr(0);

  FOR I := 0 TO (Size SHR 1)-9 DO     { Create a dounut ball sprite      }
  BEGIN
    Line(Size+1+Size SHR 1  , I,      Size+1+I,      Size SHR 1,   40-I);
    Line(Size+1+Size SHR 1+1, I,      Size+1+Size-I, Size SHR 1,   40-I);
    Line(Size+1+Size SHR 1  , Size-I, Size+1+I,      Size SHR 1+1, 40-I);
    Line(Size+1+Size SHR 1+1, Size-I, Size+1+Size-I, Size SHR 1+1, 40-I);
  END;
  GetSprite(Spr, Size+1, 0, Size+Size+1, Size);
  ConvertSpriteToRLE(Spr);

  ClrScr(0);

  SetClipBoundary(ClipX1, ClipY1, ClipX2, ClipY2);{ Set the clipping boundary }

  Line(ClipX1, ClipY1, ClipX2, ClipY1, 15);               { Draw the clipping boundary }
  Line(ClipX1, ClipY1, ClipX1, ClipY2, 15);
  Line(ClipX2, ClipY1, ClipX2, ClipY2, 15);
  Line(ClipX1, ClipY2, ClipX2, ClipY2, 15);

  WriteG1(Font, 1, 1, 'Press almost any key to continue', 7);
  WriteG1(Font, 0, 0, 'Press almost any key to continue', 15);

  DrawSprite(Spr, 160-Size DIV 2, 0);
  DrawSprite(Spr, 160-Size DIV 2, 200-Size);
  Theta := 0.0;
  StartFPS;
  WHILE NOT KeyPressed DO
  BEGIN
    Radius := Cos(Theta);
    X := Round(Cos(Theta)*110)+160 - Size DIV 2;
    Y := Round(Sin(Theta)*Radius*120)+100 - Size DIV 2;
    Theta := Theta + 0.02;
    IF Clip THEN
      DrawRLESpriteC(Spr, X, Y)
    ELSE
      DrawSprite(Spr, X, Y);

    INC(Frames);
  END;
  EndFPS;
  CloseGraph;                              { Deinit graphics                }
  Writeln('RLE Sprite Clipping Demo - (Frames = Sprites below...)');
  ShowFPS('1996');
  WRITELN;
  WRITELN('Format of command: RLEDemo [-C] [XXX]');
  WRITELN('   -C turns clipping off');
  WRITELN('   XXX changes the size of the sprite from the default of 63');
  WRITELN('      (Odd values work best, the minimum is 19)');
  WRITELN;
  WRITELN('Example: RLEDemo -C 31')
END.
-----------------] Example Ends here [-----------------


  • Created by Chris Lattner