/*
 * Copyright (c) 2021-2025 Rupert Carmichael
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

// SMS Video Display Processor

#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "smsvdp.h"

// Palette for TMS Video Modes 0-3 https://www.smspower.org/Development/Palette
static const uint32_t palette_tms[16] = {
    0xff000000, 0xff000000, 0xff00aa00, 0xff00ff00,
    0xff000055, 0xff0000ff, 0xff550000, 0xff00ffff,
    0xffaa0000, 0xffff0000, 0xff555500, 0xffffff00,
    0xff005500, 0xffff00ff, 0xff555555, 0xffffffff,
};

// The Carmichael Experience - Tweaked to Look Nice in TMS Video Modes 0-3
static const uint32_t palette_teatime[16] = {
    0xff000000, 0xff000000, 0xff23b03f, 0xff3cdf5e,
    0xff495bfe, 0xff757cff, 0xffd73218, 0xff14f8f8,
    0xffff4746, 0xffff6464, 0xffd4ce54, 0xffe6e180,
    0xff1d9a34, 0xffd63bc1, 0xffcccccc, 0xffffffff,
};

// Pointer to the palette for TMS Video Modes 0-3
static const uint32_t *pal_tms = palette_tms;

// Function pointers for drawing routines
static void (*smsvdp_bgpix)(void);
static void (*smsvdp_sprcalc)(void);
static void (*smsvdp_sprpix)(void);

// VDP Context
static smsvdp_t vdp;

// Retrieve the SMS VDP Context
smsvdp_t* smsvdp_ctx(void) {
    return &vdp;
}

// Phaser struct
static struct _phaser {
    int32_t x, y;
    int enabled;
    int offset;
    int triggered;
} phaser;

// Callback for the NMI line
static void (*smsvdp_nmi)(void);

// Set the callback for the NMI line
void smsvdp_set_nmi_callback(void (*cb)(void)) {
    smsvdp_nmi = cb;
}

// Callback to set video dimensions in the frontend
static void (*smsvdp_set_dimensions)(int, int, int, int);

// Set the callback that handles video dimension changes in the frontend
void smsvdp_set_size_callback(void (*cb)(int, int, int, int)) {
    smsvdp_set_dimensions = cb;
}

// Increment address with wrap
static inline void smsvdp_addr_inc(void) {
    vdp.addr = (vdp.addr + 1) & 0x3fff;
}

// Test if the Line Interrupt bit is set in control register 0
static inline uint8_t smsvdp_int_ln(void) {
    return vdp.ctrl[0] & 0x10;
}

// Test if the Frame Interrupt bit is set in control register 1
static inline uint8_t smsvdp_int_fr(void) {
    return vdp.ctrl[1] & 0x20;
}

/*
 * Register 0
 */

// Test if the Mode 2 bit is set in control register 0
static inline uint8_t smsvdp_m2(void) {
    return vdp.ctrl[0] & 0x02;
}

// Test if the Mode 4 bit is set in control register 0
static inline uint8_t smsvdp_m4(void) {
    return vdp.ctrl[0] & 0x04;
}

// Test if the Early Clock (Shift Sprites Left) bit is set in control register 0
static inline uint8_t smsvdp_ec(void) {
    return vdp.ctrl[0] & 0x08;
}

// Test if the Hide Left Pixels bit is set in control register 0
static inline uint8_t smsvdp_hideleft(void) {
    return vdp.ctrl[0] & 0x20;
}

// Test if the Horizontal Scroll Lock bit is set in control register 0
static inline uint8_t smsvdp_scrlock_h(void) {
    return vdp.ctrl[0] & 0x40;
}

// Test if the Vertical Scroll Lock bit is set in control register 0
static inline uint8_t smsvdp_scrlock_v(void) {
    return vdp.ctrl[0] & 0x80;
}

/*
 * Register 1
 */

// Test if the Double (Stretched) sprites bit is set in control register 1
static inline uint8_t smsvdp_spr_double(void) {
    return vdp.ctrl[1] & 0x01;
}

// Test if the Large (Tiled) sprites bit is set in control register 1
static inline uint8_t smsvdp_spr_large(void) {
    return vdp.ctrl[1] & 0x02;
}

// Test if the M3 (240-line mode) bit is set in control register 1
static inline uint8_t smsvdp_m3(void) {
    return vdp.ctrl[1] & 0x08;
}

// Test if the M1 (224-line mode) bit is set in control register 1
static inline uint8_t smsvdp_m1(void) {
    return vdp.ctrl[1] & 0x10;
}

/*
 * Misc Registers
 */

// Retrieve the current backdrop colour
static inline uint32_t smsvdp_bdcol(void) {
    // In Mode 4, the backdrop colour is taken from the sprite palette
    return smsvdp_m4() ?
        vdp.palette[(vdp.ctrl[7] & 0x0f) + 0x10] : pal_tms[vdp.ctrl[7] & 0x0f];
}

/*
 * General VDP Operations
 */

// Draw a single pixel onto the canvas
static inline void smsvdp_pixel(uint32_t c, unsigned line, unsigned dot) {
    if (line < vdp.numrenderlines && dot < SMS_VDP_WIDTH)
        vdp.vbuf[(line * SMS_VDP_WIDTH) + dot] = c;
}

// Set the video output buffer to be written to
void smsvdp_set_buffer(uint32_t *ptr) {
    vdp.vbuf = ptr;
}

void smsvdp_set_palette(unsigned p) {
    pal_tms = p ? palette_teatime : palette_tms;
}

// Set VDP Quirks
void smsvdp_set_quirks(unsigned q) {
    vdp.quirks = q;
}

// Set the region
void smsvdp_set_region(unsigned pal) {
    // 313 scanlines for PAL, 262 scanlines for NTSC
    vdp.numscanlines = pal ? SMS_VDP_SCANLINES_PAL : SMS_VDP_SCANLINES;
}

void smsvdp_set_ggmode(unsigned gg) {
    vdp.ggmode = gg;
}

// Scan for Light Phaser hits
static inline void smsvdp_phaser_scan(void) {
    int xmatch = vdp.cyc == (phaser.x + phaser.offset);
    int ymatch = abs(vdp.line - phaser.y) <= 5;

    /* If a match was found, latch HCount at this pixel. This only needs to
       happen once per line. X coordinates may match an exact pixel, but Y
       coordinates need to match for a number of lines for most games to
       register a hit.
    */
    if (xmatch && ymatch) {
        smsvdp_wr_hcount();
        phaser.triggered = 1;
    }
}

// Set the Light Phaser coordinates
void smsvdp_phaser_coords(int32_t x, int32_t y) {
    phaser.x = x;
    phaser.y = y;

    // If coordinates have been set, the Light Phaser must be enabled
    phaser.enabled = 1;
}

// Return the trigger status of the Light Phaser
int smsvdp_phaser_triggered(void) {
    return phaser.triggered;
}

uint8_t smsvdp_rd_data(void) {
    vdp.wlatch = 0; // Make sure the write latch is clear
    uint8_t rb = vdp.rdbuf; // Store original latch value
    vdp.rdbuf = vdp.vram[vdp.addr]; // Read new data into the latch
    smsvdp_addr_inc(); // Increment address
    return rb; // Return the value before the read-ahead
}

uint8_t smsvdp_rd_stat(void) {
    vdp.wlatch = 0; // Make sure the write latch is clear
    uint8_t sr = vdp.stat; // Store original register value for return
    vdp.stat &= 0x1f; // Clear INT, OVR, and COL flags on this register
    vdp.int_ln_pending = 0;
    vdp.int_fr_pending = 0;
    cega_z80_irq_clr(0); // Clear the IRQ Line
    return sr; // Return the old register value
}

// Read the currently latched HCount value
uint8_t smsvdp_rd_hcount(void) {
    return vdp.hcount;
}

// Read the current VCount value
uint8_t smsvdp_rd_vcount(void) {
    if (vdp.numscanlines == SMS_VDP_SCANLINES) { // NTSC, 262 scanlines
        if (vdp.numrenderlines == SMS_VDP_HEIGHT_EXT2) {
            // 240 line mode: 00-ff, 00-06 (Does not work on real NTSC machines)
            if (vdp.line > 0xff)
                return vdp.line & 0xff;
            else
                return vdp.line;
        }
        else if (vdp.numrenderlines == SMS_VDP_HEIGHT_EXT1) {
            // 224 line mode: 00-ea, e5-ff
            if (vdp.line > 0xea)
                return vdp.line - 6;
            else
                return vdp.line;
        }
        else {
            // 192 line mode: 00-da, d5-ff
            if (vdp.line > 0xda)
                return vdp.line - 6;
            else
                return vdp.line;
        }
    }
    else { // PAL, 313 scanlines
        if (vdp.numrenderlines == SMS_VDP_HEIGHT_EXT2) {
            // 240 line mode: 00-ff, 00-0a, d2-ff
            if (vdp.line > 0x10a)
                return vdp.line - 57;
            else if (vdp.line > 0xff)
                return vdp.line & 0xff;
            else
                return vdp.line;
        }
        else if (vdp.numrenderlines == SMS_VDP_HEIGHT_EXT1) {
            // 224 line mode: 00-ff, 00-02, ca-ff
            if (vdp.line > 0x102)
                return vdp.line - 57;
            else if (vdp.line > 0xff)
                return vdp.line & 0xff;
            else
                return vdp.line;
        }
        else {
            // 192 line mode: 00-f2, ba-ff
            if (vdp.line > 0xf2)
                return vdp.line - 57;
            else
                return vdp.line;
        }
    }
}

// Latch the HCount value based on the current line's rendering position
void smsvdp_wr_hcount(void) {
    /* HCount value is based on the number of VDP cycles run in the line (342).
       The value to be latched is the top 8 bits of this 9 bit counter, thus
       it must be shifted right once (divided by 2 again). Before these
       operations are done, the count must be offset -1, and in the case of
       being the first cycle of a line, it will wrap back to 341.
    */
    uint16_t hc = (vdp.cyc + (vdp.cyc == 0 ? 341 : -1)) >> 1;

    // At 0x93, the value jumps to 0xe9, so offset by 0x55 after 0x93.
    vdp.hcount = (hc > 0x93 ? hc + 0x55 : hc) & 0xff;
}

static inline void smsvdp_wr_palette(uint8_t addr) {
    // Convert 2-bit values to 8-bit values (0, 1, 2, 3 to 0, 85, 170, 255)
    uint8_t r = 85 * (vdp.cram[addr] & 0x03);
    uint8_t g = 85 * ((vdp.cram[addr] & 0x0c) >> 2);
    uint8_t b = 85 * ((vdp.cram[addr] & 0x30) >> 4);

    // OR the values together to create a 32-bit output value
    vdp.palette[addr] = 0xff000000 | (r << 16) | (g << 8) | b;
}

static inline void smsvdp_wr_palette_gg(uint8_t addr) {
    // Convert 4-bit values to 8-bit values (0-15 to 0-255)
    uint8_t r = 17 * (vdp.cram[addr << 1] & 0x0f);
    uint8_t g = 17 * ((vdp.cram[addr << 1] & 0xf0) >> 4);
    uint8_t b = 17 * ((vdp.cram[(addr << 1) + 1] & 0x0f));

    // OR the values together to create a 32-bit output value
    vdp.palette[addr] = 0xff000000 | (r << 16) | (g << 8) | b;
}

// Draw a single background pixel in TMS Modes 0-3
static void smsvdp_bgpix_tms(void) {
    uint32_t bg, fg; // Colour value of pal_tms entries
    uint8_t pindex = 0; // Palette Index (upper 4 bits = fg, lower 4 bits = bg)
    uint8_t chpat = 0; // One row of pixel data (Character Pattern)

    uint8_t srow = vdp.line >> 3; // Screen row being drawn (0 to 23, 8 high)
    uint8_t prow = vdp.line & 0x07; // Pattern row being drawn (0 to 7)

    uint16_t offset_col; // Colour offset
    uint16_t offset_pgen; // Pattern Generator Table address offset
    uint16_t offset_pname; // Pattern Name Table address offset

    // Screen mode
    uint8_t scrmode = ((vdp.ctrl[1] & 0x10) >> 4) | // Bit 0 (M1)
        (vdp.ctrl[0] & 0x02) | // Bit 1 (M2)
        ((vdp.ctrl[1] & 0x08) >> 1); // Bit 2 (M3)

    /* Control Register 4, which sets the Pattern Generator address offset, has
       a special function in Mode 2. Only bit 2 (PG13) sets the address of the
       Pattern Generator, resulting in either 0x0000 or 0x2000. Shift PG13 left
       11 positions to create the 14-bit address offset.
    */
    offset_pgen = (vdp.ctrl[4] & 0x04) << 11;

    // Special case for Text Mode
    if (scrmode == 0x01) {
        // In Text Mode, there are 8-pixel borders on the left and right
        if (vdp.dot < 8 || vdp.dot > 247) {
            smsvdp_pixel(smsvdp_bdcol(), vdp.line, vdp.dot);
            return;
        }

        /* VDP Control Register 7
          7  6  5  4   3  2  1  0
        ---------------------------
        | Foreground | Background | 4 bits represent the palette entry.
        ---------------------------
        */
        fg = pal_tms[(vdp.ctrl[7] >> 4) & 0x0f];
        bg = smsvdp_bdcol();

        // The screen is divided into a grid of 40 text positions aross and 24
        // down. Each of the text positions is 6 pixels wide and 8 pixels high.
        uint8_t scol = (vdp.dot - 8) / 40;
        offset_pname = vdp.vram[vdp.tbl_pname + (srow * 40) + scol];
        pindex = vdp.vram[vdp.tbl_pgen + (offset_pname << 3) + prow];

        // In Text Mode, the least significant two pixels are ignored (6x8)
        // All set bits are foreground, unset bits are background
        uint8_t p = 0x80 >> ((vdp.dot - 8) % 6);
        smsvdp_pixel(pindex & p ? fg : bg, vdp.line, vdp.dot);
        return;
    }

    // Graphics 1/2 and Multicolor Modes - Info on shifts in Datasheet, 3-3
    uint8_t scol = vdp.dot >> 3; // 256 pixels - 32 tiles, 8 pixels wide each
    if (scrmode == 0x00) { // Mode 0: Graphics 1
        offset_pname = vdp.vram[vdp.tbl_pname + (srow << 5) + scol];
        chpat = vdp.vram[vdp.tbl_pgen + (offset_pname << 3) + prow];
        pindex = vdp.vram[vdp.tbl_col + (offset_pname >> 3)];
    }
    else if (scrmode == 0x02) { // Mode 2: Graphics 2
        // In mode 2, offset is incremented by 0, 0x100, and 0x200 for each
        // 1/3 of the screen. Top = 0, Middle = 0x100, Bottom = 0x200
        offset_pname = vdp.vram[vdp.tbl_pname + (srow << 5) + scol];
        offset_pname += (srow & 0x18) << 5; // Increment if required
        offset_col = vdp.tbl_col & 0x2000;

        /* Control Register 4 bits 0 and 1 are an AND mask over the
           character number. The character number is 0 - 767 (0x2ff) and
           these two bits are ANDed over the two highest bits of this value
           (0x2ff is 10 bits, so bit 8 and 9). So in effect, if bit 0 of
           Control Register 4 is set, the second array of 256 patterns in
           the Pattern Generator table is used for the middle 8 rows
           of characters, otherwise the first 256 patterns. If bit 1 is set,
           the third chunk of patterns is used in the Pattern Generator,
           otherwise the first. OR 0xff to fill in the zeros from the shift
           operation.
        */
        uint16_t m1 = ((vdp.ctrl[4] & 0x03) << 8) | 0xff;

        /* Control Register 3 has a different meaning. Only bit 7 (CT13)
           sets the Colour Table address. Somewhat like Control Register 4
           for the Pattern Generator, bits 6 - 0 are an AND mask over the
           top 7 bits of the character number. OR 0x07 to fill in the zeros
           from the shift operation.
        */
        uint16_t m2 = ((vdp.ctrl[3] & 0x7f) << 3) | 0x07;

        // Use the masks here to select the proper pattern/colour offsets
        chpat = vdp.vram[offset_pgen + ((offset_pname & m1) << 3) + prow];
        pindex = vdp.vram[offset_col + ((offset_pname & m2) << 3) + prow];
    }
    else if (scrmode == 0x04) { // Mode 3: Multicolor
        /* 2 bytes from the Pattern Generator table represent four colours.
        The address for the first byte can be calculated as follows:
        PG + (byte in PN) x 8 + (row AND 3) x 2
        Simply increment the address by one for the second byte.

        8x8 colour block made up of 4 4x4 blocks
        -----------------------------------------
        |   7   6   5   4   |   3   2   1   0   |   One byte represents 8
        -----------------------------------------   pixels, with the 4 most
        |     Colour A      |     Colour B      |   significant bits for the
        |  PG Byte 0 >> 4   |  PG Byte 0 & 0xf  |   left pal_tms entry, and
        -----------------------------------------   4 least significant bits
        |       Colour C    |       Colour D    |   for the right entry.
        |  PG Byte 1 >> 4   |  PG Byte 1 & 0xf  |
        -----------------------------------------
        */
        offset_pname = vdp.vram[vdp.tbl_pname + (srow << 5) + scol];

        // Address of the colour offset, incremented by 1 for bottom 4 rows
        offset_col = offset_pgen + (offset_pname << 3) +
            ((srow & 0x03) << 1) + (vdp.line & 0x04 ? 1 : 0);

        pindex = vdp.vram[offset_col]; // Palette index

        // fg for left, bg for right - reusing variables for convenience
        fg = pindex >> 4 ? pal_tms[pindex >> 4] : smsvdp_bdcol();
        bg = pindex & 0x0f ? pal_tms[pindex & 0x0f] : smsvdp_bdcol();

        if ((vdp.dot % 8) < 4) // Left BG data
            smsvdp_pixel(fg, vdp.line, vdp.dot);
        else // Right BG data
            smsvdp_pixel(bg, vdp.line, vdp.dot);

        return; // Pixel already drawn, finished
    }

    // Set foreground and background values, if 0 use the backdrop colour
    bg = pindex & 0x0f ? pal_tms[pindex & 0x0f] : smsvdp_bdcol();
    fg = pindex >> 4 ? pal_tms[pindex >> 4] : smsvdp_bdcol();

    // Draw pattern data starting from the leftmost pixel
    uint8_t p = 0x80 >> (vdp.dot % 8);
    smsvdp_pixel(chpat & p ? fg : bg, vdp.line, vdp.dot);
}

// Calculate the next line of sprite pixels in TMS Modes 0-3
static void smsvdp_sprcalc_tms(void) {
    uint8_t sprmag = vdp.ctrl[1] & 0x01; // Sprites are magnified (doubled)
    uint8_t sprsize = vdp.ctrl[1] & 0x02 ? 16 : 8; // 16x16 if SI bit set

    uint8_t numspr = 0;

    for (uint8_t i = 0; i < 32; ++i) {
        /* Sprite Attribute Table Entry - Datasheet 2-25
        -------------------------------------
        |   7   6   5   4   3   2   1   0   | Bit Position
        |-----------------------------------|
        |             Y Position            | Byte 0: Vertical (Y) Position
        |-----------------------------------|
        |             X Position            | Byte 1: Horizontal (X) Position
        |-----------------------------------|
        |            Pattern Name           | Byte 2: Pattern Name (0-255)
        |-----------------------------------|
        |   EC  -   -   -   Colour Code     | Byte 3: Sprite Colour/Extra Clock
        -------------------------------------
        Notes: "Pattern Name" really refers to an index to pattern data.
               Extra Clock bit being set means decrement the X value by 32.
               Some Y positions have special meanings.
               Position 0,0 is the top left corner of the screen.
        */
        int y = vdp.vram[vdp.tbl_sattr + (i * 4)]; // "Partially signed"
        int x = vdp.vram[vdp.tbl_sattr + (i * 4) + 1];
        uint8_t pname = vdp.vram[vdp.tbl_sattr + (i * 4) + 2];
        uint8_t c = vdp.vram[vdp.tbl_sattr + (i * 4) + 3];

        // These bits are set every iteration regardless, but are only relevant
        // when the 5S bit is also set.
        vdp.stat &= ~0x1f; // Clear the FS bits (Fifth Sprite, 0-31)
        vdp.stat |= i & 0x1f; // Set FS bits to the current sprite index

        if (c & 0x80) // EC bit is set, reduce X by 32 pixels (Early Clock)
            x -= 32; // Allows sprites to be partially displayed on the left

        // If Y is 208, that sprite and all following sprites in the table are
        // not displayed. This is a Y value with a special meaning.
        if (y == 208)
            break;

        /* Y index needs to be offset by 1. Datasheet says a value of -1 puts
           the sprite "butted up at the top of the screen, touching the backdrop
           area".
        */
        y++;

        /* Wrap Y index if required - Datasheet says that a vertical
           displacement value of -31 to 0 allows a sprite to bleed in from the
           top edge of the backdrop. In this case it appears that 224 is equal
           to -31, in this "partially signed" context. 255 == 0, 254 == -1...
        */
        if (y > 224)
            y -= 256;

        // If no rows of the sprite are actually on the scanline in question,
        // this iteration is finished.
        if ((y > vdp.line) || ((y + (sprsize << sprmag)) <= vdp.line))
            continue;

        if (++numspr == 5) { // There can only be 4 sprites per scanline
            vdp.stat |= 0x40; // Set the 5S bit (Fifth Sprite detected)
            break; // We're done here, so break the loop
        }

        // If not rendering, do not go past overflow calculation
        if (!vdp.rendering)
            continue;

        // In the case of 16x16, to calculate the address in the Sprite
        // Generator table: ((pattern name) AND 252) x 8.
        if (sprsize == 16)
            pname &= 0xfc; // Do the masking here and the multiplication below

        // Calculate which row of the sprite pattern needs to be drawn
        int srow = vdp.line - y;

        // If it's magnified, divide the row in half so it will be drawn twice
        srow >>= sprmag;

        /* In the case of 8x8 sprites, there are 8 bytes for the sprite pattern,
           and there are 256 patterns in the sprite generator table.
           So simply multiply the sprite pattern by 8 to get the address.
        */
        uint8_t sppat = vdp.vram[vdp.tbl_spgen + (pname << 3) + srow];

        /* 16x16 Sprites - Datasheet 2-21
        ---------------------------------
        |  Quadrant A   |  Quadrant C   |   For 16x16 sprites, draw the pattern
        |     0x00      |     0x10      |   for Quadrants A+C or B+D on the same
        |      to       |      to       |   line. The address for the second set
        |     0x07      |     0x17      |   of pixels is offset by 16 (0x10).
        ---------------------------------
        |  Quadrant B   |  Quadrant D   |
        |     0x08      |     0x18      |
        |      to       |      to       |
        |     0x0f      |     0x1f      |
        ---------------------------------
        */

        // Loop through the sprite's pixel data - use shifts for magnification
        for (int p = 0; p < (sprsize << sprmag); ++p) {
            // Move to next iteration if the pixel is off screen, or empty
            if (((x + p) < 0) || ((x + p) >= SMS_VDP_WIDTH) || (c == 0))
                continue;

            // Handle the second pattern byte of 16x16 sprites
            if ((sprsize == 16) && (p == (8 << sprmag)))
                sppat = vdp.vram[(vdp.tbl_spgen + (pname << 3) + srow) | 0x10];

            // Check if a pixel needs to be drawn for this bit
            if (sppat & (0x80 >> ((p >> sprmag) & 7))) {
                /* Buffer sprite coincidence data (collision)
                   This has to be handled separately from pixel data because the
                   palette entry for an active pixel may be 0 (transparent). In
                   this case it is still considered for collision calculation.
                */
                if (vdp.linebuf_spr[x + p]) // Previous pixel detected
                    vdp.linebuf_col[x + p] = 1;

                // Buffer a palette index if there is neither background
                // priority nor a palette index previously buffered.
                if (!(vdp.linebuf_pri[x + p] || vdp.linebuf_spr[x + p]))
                    vdp.linebuf_spr[x + p] = c & 0x0f;
            }
        }
    }
}

// Draw a single sprite pixel in SMS TMS Modes 0-3
static void smsvdp_sprpix_tms(void) {
    // If there is collision, set the collision bit
    if (vdp.linebuf_col[vdp.dot])
        vdp.stat |= 0x20;

    // Draw the buffered palette entries as pixels on the framebuffer
    if (vdp.linebuf_spr[vdp.dot])
        smsvdp_pixel(pal_tms[vdp.linebuf_spr[vdp.dot]], vdp.line, vdp.dot);
}

// Read a palette index value from a specified tile at arbitrary X/Y coordinates
static inline unsigned smsvdp_tpix(unsigned tbase, unsigned x, unsigned y) {
    /* Tile Decoding
       Tiles are 8x8 with palette index values represented in a planar format,
       which spans 4 bytes per horizontal line. Each bit in a byte represents
       one component of a 4-bit palette entry:
       |-------------------------------|
       | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Take the tile's base address in VRAM
       |-------------------------------| and shift right by 7 minus the X offset
    v0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | in the tile to isolate the correct bit.
    v1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | Do this for 4 consecutive bytes, then
    v2 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | shift and OR the bits together to
    v3 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | create a 4-bit palette entry.
       |-------------------------------|
       Notes: Since there are 4 bytes per row of pixels, multiply the tile's
              base address by 4 to select the specific row in the tile.
    */
    unsigned v0 = (vdp.vram[(tbase + 0) + (y << 2)]) >> (7 - x);
    unsigned v1 = (vdp.vram[(tbase + 1) + (y << 2)]) >> (7 - x);
    unsigned v2 = (vdp.vram[(tbase + 2) + (y << 2)]) >> (7 - x);
    unsigned v3 = (vdp.vram[(tbase + 3) + (y << 2)]) >> (7 - x);
    return (v0 & 0x01) | (v1 & 0x01) << 1 | (v2 & 0x01) << 2 | (v3 & 0x01) << 3;
}

// Draw a single background pixel in SMS Video Mode 4
static void smsvdp_bgpix_m4(void) {
    // If the first 8 pixels are hidden, draw the backdrop colour
    if ((vdp.dot < 8) && smsvdp_hideleft()) {
        smsvdp_pixel(smsvdp_bdcol(), vdp.line, vdp.dot);
        return;
    }

    /* Determine the Background Scroll value. The 3 least significant bits are
       the fine xscroll, while the 5 most significant bits select the Screen
       Column, or Coarse X value (0-31). Modulus will allow it to wrap around
       and draw starting on the left. If the horizontal scroll lock is on, do
       not scroll the first 16 lines (0-15, or two vertical tiles).
    */
    unsigned xscroll =
        (smsvdp_scrlock_h() && (vdp.line < 16)) ? 0 : vdp.hscroll;
    xscroll = (vdp.dot - xscroll) % SMS_VDP_WIDTH;

    /* Determine the vertical scroll offset via the value in Control Register 9.
       Add the value to the scanline currently being drawn, and use modulus to
       wrap values that exceed the number of lines in the Screen Columns (224
       normally, or 256 in extended height mode).
    */
    unsigned yscroll = (vdp.line + vdp.ctrl[9]);
    yscroll %= (vdp.numrenderlines == SMS_VDP_HEIGHT) ? 224 : 256;

    // Determine the row of tiles we are working with (Screen Row, 0-27 or 0-31)
    unsigned srow = yscroll >> 3;

    // If vertical scroll lock is set when rendering colums 24-31, ignore
    // the contents of Control Register 9.
    if ((vdp.dot >= 192) && smsvdp_scrlock_v()) {
        yscroll = vdp.line;
        srow = yscroll >> 3;
    }

    /* Name Table Fetches
       |-------------------------------------------------------------------|
       |    15-14   |      13-11      |     10-6     |     5-1      |   0  |
       |-------------------------------------------------------------------|
       |   Unused   | Name Table Base |    Row (Y)   |  Column (X)  | Mask |
       |-------------------------------------------------------------------|
        Notes: Mask bit is irrelevant on the SMS2 and Game Gear
    */
    unsigned xcoarse = xscroll >> 3; // Coarse X Scroll value
    unsigned pnaddr = vdp.tbl_pname + (((srow << 5) + xcoarse) << 1);
    unsigned tdata = vdp.vram[pnaddr & vdp.pnmask] |
        (vdp.vram[(pnaddr + 1) & vdp.pnmask] << 8);

    /* Tile Data
       |-------------------------------------------------------------------|
       |   15-13   |    12    |    11   |   10  |   9   |        8-0       |
       |-------------------------------------------------------------------|
       |   Unused  | Priority | Palette | VFlip | HFlip |   Pattern Index  |
       |-------------------------------------------------------------------|
    */
    // Determine the tile's index number
    unsigned tnum = tdata & 0x01ff;

    // Tiles are 32 bytes each, so multiply by 32 to get the base address
    unsigned tilebase = tnum << 5;

    // Determine if the horizontal and vertical flip bits are set
    unsigned hflip = (tdata & 0x0200) ? 1 : 0;
    unsigned vflip = (tdata & 0x0400) ? 1 : 0;

    // Determine what half of the palette to start from (Palette Offset)
    unsigned poffset = (tdata & 0x0800) ? 0x10 : 0;

    // Determine if the priority bit is set
    unsigned priority = (tdata & 0x1000) ? 1 : 0;

    // Determine what row inside the pattern to work with (Pattern Row)
    unsigned prow = vflip ? 7 - (yscroll & 0x07) : yscroll & 0x07;

    // Draw in reverse if the horizonal flip bit is on
    unsigned x = hflip ? 7 - (xscroll & 0x07) : (xscroll & 0x07);

    // Find the palette index for this tile's x/y coordinates
    unsigned pindex = smsvdp_tpix(tilebase, x, prow);

    // Populate the priority line buffer
    if (priority && pindex)
        vdp.linebuf_pri[vdp.dot] = 1;

    // Draw the pixel onto the video buffer
    smsvdp_pixel(vdp.palette[pindex + poffset], vdp.line, vdp.dot);
}

static void smsvdp_sprcalc_m4(void) {
    uint16_t line = (vdp.line) % vdp.numscanlines;

    // No offscreen sprite processing happens until line 240
    if ((line >= vdp.numrenderlines) && (line < 240))
        return;

    // Sprite tiles are 8x8 by default, but can be 8x16, 16x16, or 16x32
    uint8_t sprheight = smsvdp_spr_large() ? 16 : 8;
    uint8_t sprwidth = 8;
    uint8_t sprmag = smsvdp_spr_double() ? 1 : 0;

    // Count the number of sprites on this line
    uint8_t numspr = 0;

    /* Sprite Attribute Table
       The Sprite Attribute Table is a 256 byte table which contains the X/Y
       coordinates and tile index number for 64 sprites.
       |-------------------------------------------------------------------|
       |       0x00-0x3f      | 0x40-0x7f |            0x80-0xff           |
       |-------------------------------------------------------------------|
       | Sprite Y Coordinates |  Unused?  | Sprite X Coordinate/Tile Index |
       |-------------------------------------------------------------------|
       Notes: The block of X coordinates and tile indices is interleaved, with
              even values for X coordinates and odd values for tile indices.
              For example, 0x80 and 0x81 correspond to the X coordinate and
              tile index for the first sprite in the table (Sprite 0).
    */
    for (int i = 0; i < 64; ++i) {
        /* X and Y Coordinates - Signed integers due to possible wrapping, and
           an additional mask is done on the Sprite Attribute Table's address
           because the Mask Bit is not used on the SMS2 or Game Gear.
        */
        int x = vdp.vram[(vdp.tbl_sattr & 0x3f00) + 0x80 + (i * 2)];
        int y = vdp.vram[(vdp.tbl_sattr & 0x3f00) + i];

        // Determine the tile index number and associated base address in VRAM
        uint8_t tnum = vdp.vram[(vdp.tbl_sattr & 0x3f00) + 0x81 + (i * 2)];
        tnum &= smsvdp_spr_large() ? 0xfe : 0xff; // Mask for tall sprites
        uint16_t tbase = (vdp.tbl_spgen & 0x2000) + (tnum << 5);

        // If the Early Clock bit is set, decrement X's value by 8
        if (smsvdp_ec())
            x -= 8;

        // Y = 208 causes all sprite processing to stop in 192 line mode
        if ((y == 208) && (vdp.numrenderlines == SMS_VDP_HEIGHT))
            break;

        // Y values are offset by one scanline (not if magnified)
        if (!sprmag)
            ++y;

        // Check if the Y value is offscreen
        uint8_t yoffscr = (y > (vdp.numrenderlines == SMS_VDP_HEIGHT ? 223 : 239));

        /* If the Y value is offscreen and the line being processed will be
           rendered, subtract 256 from the Y value to allow it to wrap to the
           top of the screen. In this case, the displayed portions of the
           sprite will trigger overflow calculation.
        */
        if (yoffscr && vdp.line < vdp.numrenderlines) {
            y -= 256;
            yoffscr = 0; // After wrapping, displayed portions are not offscreen
        }

        // If no rows of the sprite are on this line, this iteration is done
        if ((y > line) || ((y + (sprheight << sprmag)) <= line))
            continue;

        if (++numspr > 8) { // There can only be 8 sprites per scanline
            if (!yoffscr) { // Skip setting the flag if offscreen
                vdp.stat |= 0x40; // Set the OVR bit (Sprite overflow)
                break; // We're done here, so break the loop
            }
        }

        // If not rendering, do not go past overflow calculation
        if (!vdp.rendering)
            continue;

        // Calculate which row of the sprite pattern needs to be drawn
        int prow = (line - y) >> sprmag;

        for (int p = 0; p < (sprwidth << sprmag); ++p) {
            // No Collision or Drawing for offscreen X values
            if ((((x + p) < 8) && smsvdp_hideleft()) ||
                ((x + p) >= SMS_VDP_WIDTH))
                continue;

            // Determine the palette index for this pixel
            uint8_t pindex = smsvdp_tpix(tbase, (p >> sprmag), prow);

            // Detect Collision and buffer any pixels to be drawn
            if (pindex) {
                // Handle Collision Detection
                if (vdp.linebuf_spr[x + p]) // Previous pixel detected
                    vdp.linebuf_col[x + p] = 1;

                // Buffer a palette index if there is neither background
                // priority nor a palette index previously buffered.
                if (!(vdp.linebuf_pri[x + p] || vdp.linebuf_spr[x + p]))
                    vdp.linebuf_spr[x + p] = pindex + 0x10; // Sprite palette

            }
        }
    }
}

// Draw a single sprite pixel in SMS Video Mode 4
static void smsvdp_sprpix_m4(void) {
    // Draw the buffered palette entries as pixels on the framebuffer
    if (vdp.linebuf_spr[vdp.dot] && vdp.linebuf_pri[vdp.dot] != 1)
        smsvdp_pixel(vdp.palette[vdp.linebuf_spr[vdp.dot]], vdp.line, vdp.dot);
}

// Refresh the address of the Pattern Name Table in VRAM
static inline void smsvdp_pname_refresh(void) {
    if (smsvdp_m4() && (vdp.numrenderlines == SMS_VDP_HEIGHT)) // Mode 4
        vdp.tbl_pname = ((vdp.ctrl[2] & 0x0e) << 10);
    else if (vdp.numrenderlines == SMS_VDP_HEIGHT) // TMS Modes 0-3
        vdp.tbl_pname = ((vdp.ctrl[2] & 0x0f) << 10);
    else // Mask off more bits and increment by 0x700 in extended height modes
        vdp.tbl_pname = ((vdp.ctrl[2] & 0x0c) << 10) + 0x700;
}

// Set the video mode to the one specified in the Control Registers
static void smsvdp_modeset(void) {
    if (smsvdp_m4()) {
        // Set function pointers
        smsvdp_bgpix = &smsvdp_bgpix_m4;
        smsvdp_sprcalc = &smsvdp_sprcalc_m4;
        smsvdp_sprpix = &smsvdp_sprpix_m4;

        // M2 bit must be set in Mode 4 to allow M1 and M3 to change the height
        if (smsvdp_m2() && smsvdp_m1())
            vdp.numrenderlines = SMS_VDP_HEIGHT_EXT1;
        else if (smsvdp_m2() && smsvdp_m3())
            vdp.numrenderlines = SMS_VDP_HEIGHT_EXT2;
        else
            vdp.numrenderlines = SMS_VDP_HEIGHT;

        // Some hardware revisions did not support height changes
        if (vdp.quirks & SMS_VDP_QUIRK_SMS1VDP)
            vdp.numrenderlines = SMS_VDP_HEIGHT;

        // Set lower 5 VDP Status bits
        vdp.stat |= 0x1f;
    }
    else {
        // Set function pointers
        smsvdp_bgpix = &smsvdp_bgpix_tms;
        smsvdp_sprcalc = &smsvdp_sprcalc_tms;
        smsvdp_sprpix = &smsvdp_sprpix_tms;

        // Render 192 lines in TMS modes
        vdp.numrenderlines = SMS_VDP_HEIGHT;

        // Clear lower 5 VDP Status bits
        vdp.stat &= ~0x1f;
    }

    // If mode changed, Pattern Name Table Address must be refreshed
    smsvdp_pname_refresh();

    // Let the frontend know the video dimensions may have changed
    if (vdp.ggmode) // Y offset extended height mode change
        smsvdp_set_dimensions(GG_VIEWPORT_WIDTH, GG_VIEWPORT_HEIGHT, GG_XOFFSET,
            vdp.numrenderlines > SMS_VDP_HEIGHT ? GG_YOFFSET_EXT : GG_YOFFSET);
    else
        smsvdp_set_dimensions(SMS_VDP_WIDTH, vdp.numrenderlines, 0, 0);
}

// Write to a VDP register
static void smsvdp_wr_reg(uint8_t rnum, uint8_t data) {
    // Write data to the register
    vdp.ctrl[rnum] = data;

    // Bit shifts in cases 2-6 create a 14-bit address offset from the
    // start of VRAM, based on the value written to the register
    switch (rnum) {
        case 0: { // Mode Control 1
            smsvdp_modeset();

            if (vdp.int_ln_pending) {
                if (smsvdp_int_ln())
                    cega_z80_irq(0, 0xff);
                else
                    cega_z80_irq_clr(0);
            }

            break;
        }
        case 1: { // Mode Control 2
            smsvdp_modeset();

            if (vdp.int_fr_pending) {
                if (smsvdp_int_fr())
                    cega_z80_irq(0, 0xff);
                else
                    cega_z80_irq_clr(0);
            }

            break;
        }
        case 2: { // Pattern Name Table
            smsvdp_pname_refresh();
            break;
        }
        case 3: { // Colour Table
            vdp.tbl_col = (vdp.ctrl[3] << 6) & 0x3fc0;
            break;
        }
        case 4: { // Pattern Generator Table
            vdp.tbl_pgen = (vdp.ctrl[4] << 11) & 0x3800;
            break;
        }
        case 5: { // Sprite Attribute Table
            vdp.tbl_sattr = (vdp.ctrl[5] << 7) & 0x3f80;
            break;
        }
        case 6: { // Sprite Pattern Generator
            vdp.tbl_spgen = (vdp.ctrl[6] << 11) & 0x3800;
            break;
        }
        case 7: // Foreground/Backdrop Colours
            break;
        case 8: { // Background X Scroll
            if (vdp.ggmode)
                vdp.hscroll = data;
            break;
        }
        case 9: // Background Y Scroll
        case 10: // Line Counter reload value
        default: // 11-16: No Effect
            break;
    }
}

// Write to a Control Register
void smsvdp_wr_ctrl(uint8_t data) {
    if (vdp.wlatch) { // Second Write
        vdp.wlatch = 0; // Flip the latch back to indicate the write is done

        uint16_t upper = (data & 0x3f) << 8; // Upper address byte
        vdp.addr = upper | vdp.alatch; // OR the full address together

        // Set "Code" Register - 2 most significant bits of second written byte
        vdp.code = data >> 6;

        switch (data & 0xc0) { // Check if this is a register write or not
            case 0x00: { // Read VRAM data into the latch and increment address
                vdp.rdbuf = vdp.vram[vdp.addr]; // Read data into data latch
                smsvdp_addr_inc(); // Increment address
                break;
            }
            case 0x80: { // Write the address latch value into the register
                smsvdp_wr_reg(data & 0x0f, vdp.alatch); // 4 bits for register
                break;
            }
            default:
                break;
        }
    }
    else { // First Write
        vdp.wlatch = 1; // Set the write latch to indicate one byte was written
        vdp.addr = (vdp.addr & 0x3f00) | data; // Write lower address byte
        vdp.alatch = data; // Store the lower byte in the address latch
    }
}

// Write data to the VDP
void smsvdp_wr_data(uint8_t data) {
    vdp.wlatch = 0; // Make sure the write latch is clear
    vdp.rdbuf = data; // Write data to the data latch

    if (vdp.code == 0x03) { // Update CRAM/Palette entry
        if (vdp.ggmode) { // Game Gear Mode
            if (vdp.addr & 0x01) { // Odd - Write CRAM with latched and new data
                vdp.cram[vdp.addr & 0x3e] = vdp.clatch;
                vdp.cram[vdp.addr & 0x3f] = data;
                smsvdp_wr_palette_gg((vdp.addr >> 1) & 0x1f);
            }
            else { // Even - Write to CRAM Latch
                vdp.clatch = data;
            }
        }
        else { // SMS Mode
            vdp.cram[vdp.addr & 0x1f] = data;
            smsvdp_wr_palette(vdp.addr & 0x1f);
        }
    }
    else {
        vdp.vram[vdp.addr] = data; // Write data to VRAM
    }

    smsvdp_addr_inc(); // Increment Address
}

// Set initial VDP values - also could be called reset
void smsvdp_init(void) {
    // Initialize dot and master cycle values to zero
    vdp.dot = 0;
    vdp.cyc = 0;

    // Initialize rendering parameters
    vdp.numscanlines = SMS_VDP_SCANLINES;
    vdp.numrenderlines = SMS_VDP_HEIGHT;
    vdp.pnmask = 0x3fff;
    vdp.rendering = 0;

    // Set phaser offset
    phaser.offset = vdp.quirks & SMS_VDP_QUIRK_PHOFFSET ? 32 : 42;

    // Set the default line
    vdp.line = SMS_VDP_HEIGHT;

    // Enable Mode 4, Mode 2, Line Interrupts, and Hide Left 8 Pixels
    vdp.ctrl[0] = 0x36;

    // Bit 7 should always be set
    vdp.ctrl[1] = vdp.quirks & SMS_VDP_QUIRK_FRINT ? 0xa0 : 0x80;

    // Turn all bits on for some registers
    vdp.ctrl[2] = 0xff;
    vdp.ctrl[3] = 0xff;
    vdp.ctrl[4] = 0xff;
    vdp.ctrl[5] = 0xff;
    vdp.ctrl[6] = 0xff;
    vdp.ctrl[10] = 0xff;

    // Zero the Status register and Read Buffer
    vdp.stat = 0x00;
    vdp.rdbuf = 0x00;

    // Zero the VRAM
    memset(vdp.vram, 0x00, SIZE_VRAM);

    // Zero the latches and address register
    vdp.addr = 0x0000;
    vdp.code = 0x00;
    vdp.alatch = 0x00;
    vdp.clatch = 0x00;
    vdp.wlatch = 0x00;

    vdp.tbl_col = vdp.ctrl[3] << 6;
    vdp.tbl_pname = vdp.ctrl[2] << 10;
    vdp.tbl_pgen = vdp.ctrl[4] << 11;
    vdp.tbl_sattr = vdp.ctrl[5] << 7;
    vdp.tbl_spgen = vdp.ctrl[6] << 11;

    smsvdp_modeset();
}

// Process Line Interrupts
static inline void smsvdp_lineint(void) {
    if ((vdp.line <= vdp.numrenderlines) || (vdp.line == vdp.numscanlines)) {
        /* Decrement the line counter - if it underflows to 0xff (255), reload
           it with the last value written to Control Register 10. This behaviour
           relies on the line counter being an unsigned 8-bit integer.
        */
        if (--vdp.lcount == 0xff) {
            vdp.lcount = vdp.ctrl[10];
            vdp.int_ln_pending = 1;

            if (smsvdp_int_ln())
                cega_z80_irq(0, 0xff);
        }
    }
    else {
        // Reset the line counter to the reload value in Control Register 10
        // on the line after the active display period and thereafter.
        vdp.lcount = vdp.ctrl[10];
    }
}

void smsvdp_rehash(void) {
    // Set modes in case they need to change
    smsvdp_modeset();

    // Rewrite palette entries from restored CRAM
    if (vdp.ggmode) {
        for (uint8_t i = 0; i < 32; ++i)
            smsvdp_wr_palette_gg(i);
    }
    else {
        for (uint8_t i = 0; i < 32; ++i)
            smsvdp_wr_palette(i);
    }
}

// Execute a VDP cycle
int smsvdp_exec(void) {
    /* The relevent documentation used to build this was the VDPTest-ReadMe.txt
       supplied with VDPTEST.sms, and documentation from Charles MacDonald,
       including this forum post:
       https://www.smspower.org/forums/8161-SMSDisplayTiming
    */
    if (phaser.enabled && !phaser.triggered)
        smsvdp_phaser_scan();

    // Draw pixels
    if (vdp.cyc < SMS_VDP_WIDTH) {
        // Draw background/backdrop and sprites
        if (vdp.line < vdp.numrenderlines) {
            if (vdp.rendering)
                smsvdp_bgpix();
            else
                smsvdp_pixel(smsvdp_bdcol(), vdp.line, vdp.dot);
        }

        // Draw sprites, but not in TMS Text Mode
        if (smsvdp_m4())
            smsvdp_sprpix();
        else if (!smsvdp_m1())
            smsvdp_sprpix();

        // Increment the dot counter and wrap around when necessary
        vdp.dot = (vdp.dot + 1) % SMS_VDP_WIDTH;
    }

    if (vdp.cyc > 12 && vdp.cyc < 269) {
        // If there is collision, set the collision bit
        if (vdp.linebuf_col[vdp.cyc - 13])
            vdp.stat |= 0x20;
    }
    else if (vdp.cyc == 317) { // 633 to 635
        // Latch horizontal scroll value
        vdp.hscroll = vdp.ctrl[8];
    }
    else if (vdp.cyc == 320) { // 639 to 641
        // Set the Frame Interrupt bit on the Status Register
        if (vdp.line == vdp.numrenderlines) {
            vdp.stat |= 0x80;
            vdp.int_fr_pending = 1;
        }

        // Pause is handled by the VDP at the beginning of the final line.
        if ((vdp.line == (vdp.numscanlines - 1)) && !vdp.ggmode)
            smsvdp_nmi();

        // Increment the line number being worked on
        if (++vdp.line == vdp.numscanlines)
            vdp.line = 0;

        // Clear the current sprite line buffers
        for (int i = 0; i < SMS_VDP_WIDTH; ++i) {
            vdp.linebuf_col[i] = 0x00;
            vdp.linebuf_pri[i] = 0x00;
            vdp.linebuf_spr[i] = 0x00;
        }

        // Calculate sprite properties for the next line
        smsvdp_sprcalc();

        // Determine whether to render background/sprites for the next scanline
        vdp.rendering = vdp.ctrl[1] & 0x40;
    }
    else if (vdp.cyc == 321) { // 642 to 644
        // Generate a Frame Interrupt if they are allowed
        if (vdp.line == vdp.numrenderlines + 1) {
            vdp.stat |= 0x80;
            if (smsvdp_int_fr())
                cega_z80_irq(0, 0xff);
        }
    }
    else if (vdp.cyc == 323) { // 645 to 647
        // Do any line interrupt related operations
        smsvdp_lineint();
    }

    // This is the end of the line - reset the cycle counter
    if (++vdp.cyc == 342) {
        // Reset VDP master cycle counter
        vdp.cyc = 0;

        // Allow the phaser to latch HCount on the next line
        phaser.triggered = 0;

        // Clear the priority buffer for this scanline
        for (int i = 0; i < SMS_VDP_WIDTH; ++i)
            vdp.linebuf_pri[i] = 0x00;

        // Set Pattern Name Table Mask for Tilemap Mirroring
        if (vdp.quirks & SMS_VDP_QUIRK_SMS1VDP)
            vdp.pnmask = (vdp.ctrl[2] & 0x01) ? 0x3fff : 0x3bff;

        // When the final scanline has been drawn, start over again
        if (vdp.line == 0)
            return 1; // Signal that an entire frame has been drawn
    }

    return 0; // Still drawing the frame
}
