Drawing window shapes

From SNESdev Wiki
Revision as of 02:07, 3 August 2022 by Undisbeliever (talk | contribs) (Added HDMA table design considerations, rectangle examples and trapezium examples (with pseudo-code))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

Shaped windows can be created using:

  • One HDMA channel in 2 registers write once mode to WH0 and WH1.
  • Two HDMA channels in 1 register write once mode (ie, one channel to WH0, the other to WH1).


The following considerations will need to be made when building a HDMA table for the window registers:

  • A dynamic HDMA table must be double buffered to prevent glitches and/or screen tearing.
  • HDMA table entries are a maximum 127 scanlines long. All segments/lines/shapes greater then 127 scanlines tall must be split into two. Alternatively, indirect HDMA can be used to map a contiguous buffer to two HDMA repeat mode entries.
  • The left and right window positions must be clamped to an 8 bit value (0-255).
    • The position must be 0 if the calculated value is < 0 (underflows).
    • The position must be 255 if the calculated value is >= 256 (overflows).
  • The code should detect if the window's scanline is horizontally offscreen. Failure to do so can result in a 1-pixel glitched window on the left or right side of the screen.
    • The offscreen test is not required if every scanline of the window is guaranteed to be horizontally-onscreen (ie, a circle with a centre X-position between 0-255).


Windowless section

A window is considered empty (or offscreen) if the left position is greater than the right position. By creating a non-repeat HDMA table entry with a left position > 0 and a right position of 0, the window can be disabled for multiple scanlines. The HDMA table must end with an empty window if the window shape does not cover the entire display height.


Rectangles

The simplest HDMA window shape is a rectangle. It consists of four segments:

  • An optional windowless section. This moves the rectangle downwards.
  • An active window section. This sets the left and right window position to the left and right screen coordinates of the rectangle.
  • Another windowless section, lasting a single scanline, ending the shape.
  • An end of HDMA table byte.

All segments can be built using a non-repeating HDMA table entries.


Pseudo-code:

function hdma_rectangle_window(yPos, height, left, right) -> hdma_table:
    // INPUT: yPos   - the vertical offset (0 <= yPos <= 254)
    // INPUT: height - the height of the rectangle (1 <= height <= 254)
    // INPUT: left   - the left position of the rectangle (0 <= left <= 254)
    // INPUT: right  - the right position of the rectangle (left + 1 <= right <= 255)
    //
    // OUTPUT: A 2-registers-write-once HDMA table for the window position registers.

    hdma_table = next_hdma_buffer()

    buffer = hdma_table

    if yPos > 0:
        // Disable window for `yPos` scanlines
        buffer = non_repeating_entry(buffer, yPos, 255, 0)

    // Enable window
    buffer = non_repeating_entry(buffer, height, left, right)

    // Disable window
    buffer = non_repeating_entry(buffer, 1, 255, 0)

    // End HDMA table
    *buffer++ = 0

    return hdma_table



function non_repeating_entry(buffer, n, left, right) -> buffer:
    // Adds one or two HDMA entries to `buffer`, setting the `left` and `right` window position for `n` scanlines
    //
    // INPUT: buffer    - buffer to store the 2-registers-write-once HDMA table
    // INPUT: n         - number of scanlines (0 <= n <= 254)
    // INPUT: left      - window left position (0 <= left <= 255)
    // INPUT: right     - window right position (0 <= right <= 255)
    //
    // OUTPUT: buffer position after HDMA entries

    assert(n > 0)
    assert(n <= 127 * 2)

    if n <= 127:
        // One HDMA entry
        *buffer++ = n
        *buffer++ = left
        *buffer++ = right
    else:
        // Write two HDMA entries
        *buffer++ = 127
        *buffer++ = left
        *buffer++ = right

        *buffer++ = n - 127
        *buffer++ = left
        *buffer++ = right

    return buffer


Trapeziums

All straight lined single-window shapes can be built using one or more trapeziums.


There are multiple ways of drawing the sides of a trapezium:

  • Bresenham's line drawing algorithm.
  • Adding or subtracting an 8.8 fixed-point delta-X to the left/right position for every scanline. This is faster (but less accurate) then Bresenham's algorithm.
    • Using separate code-paths for positive and negative delta-X values will simplify the 8-bit clipping code.
  • Incrementing or decrementing an 8-bit position by a fixed value every scanline. This is the fastest method, but it can only be used to draw lines with fixed integer ratios.
    • Alternatively, the window positions can be repeated every N scanlines to draw steep lines.
  • Using a table of x-position deltas in ROM. For example; the delta-table for a 10 degree line is [ 5, 6, 6 ] and the delta table for a 60 degree line is [ 1, 1, 0, 1, 0, 1, 0 ]. Delta-tables cannot be used to draw arbitrary lines.
    • It is recommended that delta-tables only contain unsigned numbers to avoid signed comparisons. Two drawing functions, one for incrementing X-positions and another for decrementing X-positions can halve the amount of ROM space required by taking advantage of line symmetry.
    • Delta-tables are not limited to straight lines, they can also be used to draw curves and circles.


Unoptimised trapezium drawing pseudo-code (using the fixed-point delta-X method):

function draw_trapezium_window(buffer, height, left, left_dx, right, right_dx) -> left, right, buffer:
    // Builds a HDMA repeat-mode entry for a trapezium section.
    //
    // ASSUMES: left <= right
    //
    // INPUT: buffer    - buffer to store the 2-registers-write-once HDMA table
    // INPUT: height    - number of scanlines (1 <= height <= 127)
    // INPUT: left      - current left position (signed .8 fixed-point)
    // INPUT: dx_left   - delta-X for the left side of the window (signed .8 fixed-point)
    // INPUT: right     - current right position (signed .8 fixed-point)
    // INPUT: dx_right  - delta-X for the right side of the window (signed .8 fixed-point)

    // OUTPUT: new left position (signed .8 fixed-point integer)
    // OUTPUT: new right position (signed .8 fixed-point integer)
    // OUTPUT: buffer after entries

    assert(height > 0 and height <= 127)

    // Repeat mode, `height` scanlines
    *buffer++ = 0x80 | height

    repeat `height` times:
        left = left + dx_left
        right = right + dx_right

        // Test if the window is visible (assumes left <= right)
        if right < 0.0 or left >= 0x100.00:
            // Window is off-screen
            *buffer++ = 255
            *buffer++ = 0
        else:
            // Window is on-screen

            // clamp signed .8 fixed-point positions to 8 bit unsigned bytes
            l = left >> 8
            if l < 0:
                l = 0
            r = right >> 8
            if r > 255
                r = 255

            *buffer++ = l
            *buffer++ = r

    return left, right, buffer


Acute Trapeziums

An acute trapezium offers a few advantages over an obtuse trapezium.

  • The off-screen window test is not required if the short base of an acute trapezium is on-screen.
  • If the short base is at the top, the left side is always moving left and the right side is always moving right, simplifying both the position calculation and the 8-bit clamping. Once a position has been clamped, all subsequent positions will remain 0 for the left and/or 255 for the right.
  • If the short base is at the bottom, the acute trapezium can be built in reverse, bottom to top, to simplify the 8-bit clamping.


Examples

A triangle with a horizontal base can be created from a single acute trapezium with a very short base.

Window triangle horizontal base.svg


A triangle pointing right (centre_x > top_x) is made up of two trapeziums. The first trapezium has a short top base and bottom base of indeterminate width. The second trapezium immediately follows the first; with the left side's slope matching the first trapezium and the right side sloping at a different angle.

Triangles pointing left (centre_x < top_x) are a mirrored form of triangles pointing right.

Window right triangle.svg


Multiple trapeziums can be combined to create convex polygons.

Window quad.svg


Unrotated diamonds are built using two acute trapeziums. The second trapezium is vertical mirror of the first.

Window diamond.svg


Unrotated Octagons are built using two acute trapeziums, separated by a rectangular section. The second trapezium is a vertical mirror of the first.

Window octagon.svg