/*
Copyright (C) 1997-2008 ZSNES Team ( zsKnight, _Demo_, pagefault, Nach )

http://www.zsnes.com
http://sourceforge.net/projects/zsnes
https://zsnes.bountysource.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "dsp4emu.h"
#include <stdbool.h>
#include <string.h>

/*
Due recognition and credit are given on Overload's DSP website.
Thank those contributors for their hard work on this chip.


Fixed-point math reminder:

[sign, integer, fraction]
1.15.00 * 1.15.00 = 2.30.00 -> 1.30.00 (DSP) -> 1.31.00 (LSB is '0')
1.15.00 * 1.00.15 = 2.15.15 -> 1.15.15 (DSP) -> 1.15.16 (LSB is '0')
*/

#define READ_WORD(s) (*(uint16_t*)(s))
#define READ_DWORD(s) (*(uint32_t*)(s))
#define WRITE_WORD(s, d) (*(uint16_t*)(s)) = (d)
#define WRITE_DWORD(s, d) (*(uint32_t*)(s)) = (d)

struct DSP4_t DSP4;
struct DSP4_vars_t DSP4_vars;

//////////////////////////////////////////////////////////////

// input protocol

static int16_t DSP4_READ_WORD()
{
    int16_t out;

    out = READ_WORD(DSP4.parameters + DSP4.in_index);
    DSP4.in_index += 2;

    return out;
}

static int32_t DSP4_READ_DWORD()
{
    int32_t out;

    out = READ_DWORD(DSP4.parameters + DSP4.in_index);
    DSP4.in_index += 4;

    return out;
}

//////////////////////////////////////////////////////////////

// output protocol

#define DSP4_CLEAR_OUT()    \
    {                       \
        DSP4.out_count = 0; \
        DSP4.out_index = 0; \
    }

#define DSP4_WRITE_BYTE(d)                             \
    {                                                  \
        WRITE_WORD(DSP4.output + DSP4.out_count, (d)); \
        DSP4.out_count++;                              \
    }

#define DSP4_WRITE_WORD(d)                             \
    {                                                  \
        WRITE_WORD(DSP4.output + DSP4.out_count, (d)); \
        DSP4.out_count += 2;                           \
    }

#ifndef MSB_FIRST
#define DSP4_WRITE_16_WORD(d)                          \
    {                                                  \
        memcpy(DSP4.output + DSP4.out_count, (d), 32); \
        DSP4.out_count += 32;                          \
    }
#else
#define DSP4_WRITE_16_WORD(d)                             \
    {                                                     \
        int16_t *p = (d), *end = (d) + 16;                \
        for (; p != end; p++) {                           \
            WRITE_WORD(DSP4.output + DSP4.out_count, *p); \
        }                                                 \
        DSP4.out_count += 32;                             \
    }
#endif

#ifdef PRINT_OP
#define DSP4_WRITE_DEBUG(x, d) \
    WRITE_WORD(nop + x, d);
#endif

#ifdef DEBUG_DSP
#define DSP4_WRITE_DEBUG(x, d) \
    WRITE_WORD(nop + x, d);
#endif

//////////////////////////////////////////////////////////////

// used to wait for dsp i/o

#define DSP4_WAIT(x)          \
    DSP4.in_index = 0;        \
    DSP4_vars.DSP4_Logic = x; \
    return;

//////////////////////////////////////////////////////////////

// 1.7.8 -> 1.15.16
#define SEX78(a) (((int32_t)((int16_t)(a))) << 8)

// 1.15.0 -> 1.15.16
#define SEX16(a) (((int32_t)((int16_t)(a))) << 16)

#ifdef PRINT_OP
#define U16(a) ((uint16_t)(a))
#endif

#ifdef DEBUG_DSP
#define U16(a) ((uint16_t)(a))
#endif

//////////////////////////////////////////////////////////////

// Attention: This lookup table is not verified
static const uint16_t div_lut[64] = {
    0x0000,
    0x8000,
    0x4000,
    0x2aaa,
    0x2000,
    0x1999,
    0x1555,
    0x1249,
    0x1000,
    0x0e38,
    0x0ccc,
    0x0ba2,
    0x0aaa,
    0x09d8,
    0x0924,
    0x0888,
    0x0800,
    0x0787,
    0x071c,
    0x06bc,
    0x0666,
    0x0618,
    0x05d1,
    0x0590,
    0x0555,
    0x051e,
    0x04ec,
    0x04bd,
    0x0492,
    0x0469,
    0x0444,
    0x0421,
    0x0400,
    0x03e0,
    0x03c3,
    0x03a8,
    0x038e,
    0x0375,
    0x035e,
    0x0348,
    0x0333,
    0x031f,
    0x030c,
    0x02fa,
    0x02e8,
    0x02d8,
    0x02c8,
    0x02b9,
    0x02aa,
    0x029c,
    0x028f,
    0x0282,
    0x0276,
    0x026a,
    0x025e,
    0x0253,
    0x0249,
    0x023e,
    0x0234,
    0x022b,
    0x0222,
    0x0219,
    0x0210,
    0x0208,
};
int16_t DSP4_Inverse(int16_t value)
{
    // saturate bounds
    if (value < 0) {
        value = 0;
    }
    if (value > 63) {
        value = 63;
    }

    return div_lut[value];
}

//////////////////////////////////////////////////////////////

// Prototype
void DSP4_OP0B(bool* draw, int16_t sp_x, int16_t sp_y, int16_t sp_attr, bool size, bool stop);

//////////////////////////////////////////////////////////////

// OP00
void DSP4_Multiply(int16_t Multiplicand, int16_t Multiplier, int32_t* Product)
{
    *Product = (Multiplicand * Multiplier << 1) >> 1;
}

//////////////////////////////////////////////////////////////

void DSP4_OP01()
{
    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    case 3:
        goto resume3;
        break;
    }

    ////////////////////////////////////////////////////
    // process initial inputs

    // sort inputs
    DSP4_vars.world_y = DSP4_READ_DWORD();
    DSP4_vars.poly_bottom[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][0] = DSP4_READ_WORD();
    DSP4_vars.viewport_bottom = DSP4_READ_WORD();
    DSP4_vars.world_x = DSP4_READ_DWORD();
    DSP4_vars.poly_cx[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[0][0] = DSP4_READ_WORD();
    DSP4_vars.world_yofs = DSP4_READ_WORD();
    DSP4_vars.world_dy = DSP4_READ_DWORD();
    DSP4_vars.world_dx = DSP4_READ_DWORD();
    DSP4_vars.distance = DSP4_READ_WORD();
    DSP4_READ_WORD(); // 0x0000
    DSP4_vars.world_xenv = DSP4_READ_DWORD();
    DSP4_vars.world_ddy = DSP4_READ_WORD();
    DSP4_vars.world_ddx = DSP4_READ_WORD();
    DSP4_vars.view_yofsenv = DSP4_READ_WORD();

    // initial (x,y,offset) at starting DSP4_vars.raster line
    DSP4_vars.view_x1 = (int16_t)((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16);
    DSP4_vars.view_y1 = (int16_t)(DSP4_vars.world_y >> 16);
    DSP4_vars.view_xofs1 = (int16_t)(DSP4_vars.world_x >> 16);
    DSP4_vars.view_yofs1 = DSP4_vars.world_yofs;
    DSP4_vars.view_turnoff_x = 0;
    DSP4_vars.view_turnoff_dx = 0;

    // first DSP4_vars.raster line
    DSP4_vars.poly_raster[0][0] = DSP4_vars.poly_bottom[0][0];

    do {
        ////////////////////////////////////////////////////
        // process one iteration of projection

        // perspective projection of world (x,y,scroll) points
        // based on the current projection lines
        DSP4_vars.view_x2 = (int16_t)((((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16) * DSP4_vars.distance >> 15) + (DSP4_vars.view_turnoff_x * DSP4_vars.distance >> 15));
        DSP4_vars.view_y2 = (int16_t)((DSP4_vars.world_y >> 16) * DSP4_vars.distance >> 15);
        DSP4_vars.view_xofs2 = DSP4_vars.view_x2;
        DSP4_vars.view_yofs2 = (DSP4_vars.world_yofs * DSP4_vars.distance >> 15) + DSP4_vars.poly_bottom[0][0] - DSP4_vars.view_y2;

        // 1. World x-location before transformation
        // 2. Viewer x-position at the next
        // 3. World y-location before perspective projection
        // 4. Viewer y-position below the horizon
        // 5. Number of DSP4_vars.raster lines drawn in this iteration

        DSP4_CLEAR_OUT();
        DSP4_WRITE_WORD((uint16_t)((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16));
        DSP4_WRITE_WORD(DSP4_vars.view_x2);
        DSP4_WRITE_WORD((uint16_t)(DSP4_vars.world_y >> 16));
        DSP4_WRITE_WORD(DSP4_vars.view_y2);

        //////////////////////////////////////////////////////

        // SR = 0x00

        // determine # of DSP4_vars.raster lines used
        DSP4_vars.segments = DSP4_vars.poly_raster[0][0] - DSP4_vars.view_y2;

        // prevent overdraw
        if (DSP4_vars.view_y2 >= DSP4_vars.poly_raster[0][0])
            DSP4_vars.segments = 0;
        else
            DSP4_vars.poly_raster[0][0] = DSP4_vars.view_y2;

        // don't draw outside the window
        if (DSP4_vars.view_y2 < DSP4_vars.poly_top[0][0]) {
            DSP4_vars.segments = 0;

            // flush remaining DSP4_vars.raster lines
            if (DSP4_vars.view_y1 >= DSP4_vars.poly_top[0][0])
                DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.poly_top[0][0];
        }

        // SR = 0x80

        DSP4_WRITE_WORD(DSP4_vars.segments);

        //////////////////////////////////////////////////////

        // scan next command if no SR check needed
        if (DSP4_vars.segments) {
            int32_t px_dx, py_dy;
            int32_t x_scroll, y_scroll;

            // SR = 0x00

            // linear interpolation (lerp) between projected points
            px_dx = (DSP4_vars.view_xofs2 - DSP4_vars.view_xofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;
            py_dy = (DSP4_vars.view_yofs2 - DSP4_vars.view_yofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;

            // starting step values
            x_scroll = SEX16(DSP4_vars.poly_cx[0][0] + DSP4_vars.view_xofs1);
            y_scroll = SEX16(-DSP4_vars.viewport_bottom + DSP4_vars.view_yofs1 + DSP4_vars.view_yofsenv + DSP4_vars.poly_cx[1][0] - DSP4_vars.world_yofs);

            // SR = 0x80

            // rasterize line
            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < DSP4_vars.segments; DSP4_vars.lcv++) {
                // 1. HDMA memory pointer (bg1)
                // 2. vertical scroll offset ($210E)
                // 3. horizontal scroll offset ($210D)

                DSP4_WRITE_WORD(DSP4_vars.poly_ptr[0][0]);
                DSP4_WRITE_WORD((uint16_t)((y_scroll + 0x8000) >> 16));
                DSP4_WRITE_WORD((uint16_t)((x_scroll + 0x8000) >> 16));

                // update memory address
                DSP4_vars.poly_ptr[0][0] -= 4;

                // update screen values
                x_scroll += px_dx;
                y_scroll += py_dy;
            }
        }

        ////////////////////////////////////////////////////
        // Post-update

        // update new viewer (x,y,scroll) to last DSP4_vars.raster line drawn
        DSP4_vars.view_x1 = DSP4_vars.view_x2;
        DSP4_vars.view_y1 = DSP4_vars.view_y2;
        DSP4_vars.view_xofs1 = DSP4_vars.view_xofs2;
        DSP4_vars.view_yofs1 = DSP4_vars.view_yofs2;

        // add deltas for projection lines
        DSP4_vars.world_dx += SEX78(DSP4_vars.world_ddx);
        DSP4_vars.world_dy += SEX78(DSP4_vars.world_ddy);

        // update projection lines
        DSP4_vars.world_x += (DSP4_vars.world_dx + DSP4_vars.world_xenv);
        DSP4_vars.world_y += DSP4_vars.world_dy;

        // update road turnoff position
        DSP4_vars.view_turnoff_x += DSP4_vars.view_turnoff_dx;

        ////////////////////////////////////////////////////
        // command check

        // scan next command
        DSP4.in_count = 2;
        DSP4_WAIT(1)
    resume1:

        // check for termination
        DSP4_vars.distance
            = DSP4_READ_WORD();
        if (DSP4_vars.distance == -0x8000)
            break;

        // road turnoff
        if ((uint16_t)DSP4_vars.distance == 0x8001) {
            DSP4.in_count = 6;
            DSP4_WAIT(2)
        resume2:

            DSP4_vars.distance
                = DSP4_READ_WORD();
            DSP4_vars.view_turnoff_x = DSP4_READ_WORD();
            DSP4_vars.view_turnoff_dx = DSP4_READ_WORD();

            // factor in new changes
            DSP4_vars.view_x1 += (DSP4_vars.view_turnoff_x * DSP4_vars.distance >> 15);
            DSP4_vars.view_xofs1 += (DSP4_vars.view_turnoff_x * DSP4_vars.distance >> 15);

            // update stepping values
            DSP4_vars.view_turnoff_x += DSP4_vars.view_turnoff_dx;

            DSP4.in_count = 2;
            DSP4_WAIT(1)
        }

        // already have 2 bytes read
        DSP4.in_count = 6;
        DSP4_WAIT(3)
    resume3:

        // inspect inputs
        DSP4_vars.world_ddy
            = DSP4_READ_WORD();
        DSP4_vars.world_ddx = DSP4_READ_WORD();
        DSP4_vars.view_yofsenv = DSP4_READ_WORD();

        // no envelope here
        DSP4_vars.world_xenv = 0;
    } while (1);

    // terminate op
    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

void DSP4_OP03()
{
    DSP4_vars.OAM_RowMax = 33;
    memset(DSP4_vars.OAM_Row, 0, 64);
}

//////////////////////////////////////////////////////////////

void DSP4_OP05()
{
    DSP4_vars.OAM_index = 0;
    DSP4_vars.OAM_bits = 0;
    memset(DSP4_vars.OAM_attr, 0, 32);
    DSP4_vars.sprite_count = 0;
}

//////////////////////////////////////////////////////////////

void DSP4_OP06()
{
    DSP4_CLEAR_OUT();
    DSP4_WRITE_16_WORD(DSP4_vars.OAM_attr);
}

//////////////////////////////////////////////////////////////

void DSP4_OP07()
{
    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    }

    ////////////////////////////////////////////////////
    // sort inputs

    DSP4_vars.world_y = DSP4_READ_DWORD();
    DSP4_vars.poly_bottom[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][0] = DSP4_READ_WORD();
    DSP4_vars.viewport_bottom = DSP4_READ_WORD();
    DSP4_vars.world_x = DSP4_READ_DWORD();
    DSP4_vars.poly_cx[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[0][0] = DSP4_READ_WORD();
    DSP4_vars.world_yofs = DSP4_READ_WORD();
    DSP4_vars.distance = DSP4_READ_WORD();
    DSP4_vars.view_y2 = DSP4_READ_WORD();
    DSP4_vars.view_dy = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
    DSP4_vars.view_x2 = DSP4_READ_WORD();
    DSP4_vars.view_dx = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
    DSP4_vars.view_yofsenv = DSP4_READ_WORD();

    // initial (x,y,offset) at starting DSP4_vars.raster line
    DSP4_vars.view_x1 = (int16_t)(DSP4_vars.world_x >> 16);
    DSP4_vars.view_y1 = (int16_t)(DSP4_vars.world_y >> 16);
    DSP4_vars.view_xofs1 = DSP4_vars.view_x1;
    DSP4_vars.view_yofs1 = DSP4_vars.world_yofs;

    // first DSP4_vars.raster line
    DSP4_vars.poly_raster[0][0] = DSP4_vars.poly_bottom[0][0];

    do {
        ////////////////////////////////////////////////////
        // process one iteration of projection

        // add shaping
        DSP4_vars.view_x2 += DSP4_vars.view_dx;
        DSP4_vars.view_y2 += DSP4_vars.view_dy;

        // vertical scroll calculation
        DSP4_vars.view_xofs2 = DSP4_vars.view_x2;
        DSP4_vars.view_yofs2 = (DSP4_vars.world_yofs * DSP4_vars.distance >> 15) + DSP4_vars.poly_bottom[0][0] - DSP4_vars.view_y2;

        // 1. Viewer x-position at the next
        // 2. Viewer y-position below the horizon
        // 3. Number of DSP4_vars.raster lines drawn in this iteration

        DSP4_CLEAR_OUT();
        DSP4_WRITE_WORD(DSP4_vars.view_x2);
        DSP4_WRITE_WORD(DSP4_vars.view_y2);

        //////////////////////////////////////////////////////

        // SR = 0x00

        // determine # of DSP4_vars.raster lines used
        DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.view_y2;

        // prevent overdraw
        if (DSP4_vars.view_y2 >= DSP4_vars.poly_raster[0][0])
            DSP4_vars.segments = 0;
        else
            DSP4_vars.poly_raster[0][0] = DSP4_vars.view_y2;

        // don't draw outside the window
        if (DSP4_vars.view_y2 < DSP4_vars.poly_top[0][0]) {
            DSP4_vars.segments = 0;

            // flush remaining DSP4_vars.raster lines
            if (DSP4_vars.view_y1 >= DSP4_vars.poly_top[0][0])
                DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.poly_top[0][0];
        }

        // SR = 0x80

        DSP4_WRITE_WORD(DSP4_vars.segments);

        //////////////////////////////////////////////////////

        // scan next command if no SR check needed
        if (DSP4_vars.segments) {
            int32_t px_dx, py_dy;
            int32_t x_scroll, y_scroll;

            // SR = 0x00

            // linear interpolation (lerp) between projected points
            px_dx = (DSP4_vars.view_xofs2 - DSP4_vars.view_xofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;
            py_dy = (DSP4_vars.view_yofs2 - DSP4_vars.view_yofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;

            // starting step values
            x_scroll = SEX16(DSP4_vars.poly_cx[0][0] + DSP4_vars.view_xofs1);
            y_scroll = SEX16(-DSP4_vars.viewport_bottom + DSP4_vars.view_yofs1 + DSP4_vars.view_yofsenv + DSP4_vars.poly_cx[1][0] - DSP4_vars.world_yofs);

            // SR = 0x80

            // rasterize line
            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < DSP4_vars.segments; DSP4_vars.lcv++) {
                // 1. HDMA memory pointer (bg2)
                // 2. vertical scroll offset ($2110)
                // 3. horizontal scroll offset ($210F)

                DSP4_WRITE_WORD(DSP4_vars.poly_ptr[0][0]);
                DSP4_WRITE_WORD((uint16_t)((y_scroll + 0x8000) >> 16));
                DSP4_WRITE_WORD((uint16_t)((x_scroll + 0x8000) >> 16));

                // update memory address
                DSP4_vars.poly_ptr[0][0] -= 4;

                // update screen values
                x_scroll += px_dx;
                y_scroll += py_dy;
            }
        }

        /////////////////////////////////////////////////////
        // Post-update

        // update new viewer (x,y,scroll) to last DSP4_vars.raster line drawn
        DSP4_vars.view_x1 = DSP4_vars.view_x2;
        DSP4_vars.view_y1 = DSP4_vars.view_y2;
        DSP4_vars.view_xofs1 = DSP4_vars.view_xofs2;
        DSP4_vars.view_yofs1 = DSP4_vars.view_yofs2;

        ////////////////////////////////////////////////////
        // command check

        // scan next command
        DSP4.in_count = 2;
        DSP4_WAIT(1)
    resume1:

        // check for opcode termination
        DSP4_vars.distance
            = DSP4_READ_WORD();
        if (DSP4_vars.distance == -0x8000)
            break;

        // already have 2 bytes in queue
        DSP4.in_count = 10;
        DSP4_WAIT(2)
    resume2:

        // inspect inputs
        DSP4_vars.view_y2
            = DSP4_READ_WORD();
        DSP4_vars.view_dy = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
        DSP4_vars.view_x2 = DSP4_READ_WORD();
        DSP4_vars.view_dx = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
        DSP4_vars.view_yofsenv = DSP4_READ_WORD();
    } while (1);

    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

void DSP4_OP08()
{
    int16_t win_left, win_right;
    int16_t view_x[2], view_y[2];
    int16_t envelope[2][2];

    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    }

    ////////////////////////////////////////////////////
    // process initial inputs for two polygons

    // clip values
    DSP4_vars.poly_clipRt[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_clipRt[0][1] = DSP4_READ_WORD();
    DSP4_vars.poly_clipRt[1][0] = DSP4_READ_WORD();
    DSP4_vars.poly_clipRt[1][1] = DSP4_READ_WORD();

    DSP4_vars.poly_clipLf[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_clipLf[0][1] = DSP4_READ_WORD();
    DSP4_vars.poly_clipLf[1][0] = DSP4_READ_WORD();
    DSP4_vars.poly_clipLf[1][1] = DSP4_READ_WORD();

    // unknown (constant) (ex. 1P/2P = $00A6, $00A6, $00A6, $00A6)
    DSP4_READ_WORD();
    DSP4_READ_WORD();
    DSP4_READ_WORD();
    DSP4_READ_WORD();

    // unknown (constant) (ex. 1P/2P = $00A5, $00A5, $00A7, $00A7)
    DSP4_READ_WORD();
    DSP4_READ_WORD();
    DSP4_READ_WORD();
    DSP4_READ_WORD();

    // polygon centering (left,right)
    DSP4_vars.poly_cx[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[0][1] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][1] = DSP4_READ_WORD();

    // HDMA pointer locations
    DSP4_vars.poly_ptr[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[0][1] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[1][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[1][1] = DSP4_READ_WORD();

    // starting DSP4_vars.raster line below the horizon
    DSP4_vars.poly_bottom[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_bottom[0][1] = DSP4_READ_WORD();
    DSP4_vars.poly_bottom[1][0] = DSP4_READ_WORD();
    DSP4_vars.poly_bottom[1][1] = DSP4_READ_WORD();

    // top boundary line to clip
    DSP4_vars.poly_top[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[0][1] = DSP4_READ_WORD();
    DSP4_vars.poly_top[1][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[1][1] = DSP4_READ_WORD();

    // unknown
    // (ex. 1P = $2FC8, $0034, $FF5C, $0035)
    //
    // (ex. 2P = $3178, $0034, $FFCC, $0035)
    // (ex. 2P = $2FC8, $0034, $FFCC, $0035)

    DSP4_READ_WORD();
    DSP4_READ_WORD();
    DSP4_READ_WORD();
    DSP4_READ_WORD();

    // look at guidelines for both polygon shapes
    DSP4_vars.distance = DSP4_READ_WORD();
    view_x[0] = DSP4_READ_WORD();
    view_y[0] = DSP4_READ_WORD();
    view_x[1] = DSP4_READ_WORD();
    view_y[1] = DSP4_READ_WORD();

    // envelope shaping guidelines (one frame only)
    envelope[0][0] = DSP4_READ_WORD();
    envelope[0][1] = DSP4_READ_WORD();
    envelope[1][0] = DSP4_READ_WORD();
    envelope[1][1] = DSP4_READ_WORD();

    // starting base values to project from
    DSP4_vars.poly_start[0] = view_x[0];
    DSP4_vars.poly_start[1] = view_x[1];

    // starting DSP4_vars.raster lines to begin drawing
    DSP4_vars.poly_raster[0][0] = view_y[0];
    DSP4_vars.poly_raster[0][1] = view_y[0];
    DSP4_vars.poly_raster[1][0] = view_y[1];
    DSP4_vars.poly_raster[1][1] = view_y[1];

    // starting distances
    DSP4_vars.poly_plane[0] = DSP4_vars.distance;
    DSP4_vars.poly_plane[1] = DSP4_vars.distance;

    // SR = 0x00

    // re-center coordinates
    win_left = DSP4_vars.poly_cx[0][0] - view_x[0] + envelope[0][0];
    win_right = DSP4_vars.poly_cx[0][1] - view_x[0] + envelope[0][1];

    // saturate offscreen data for polygon #1
    if (win_left < DSP4_vars.poly_clipLf[0][0]) {
        win_left = DSP4_vars.poly_clipLf[0][0];
    }
    if (win_left > DSP4_vars.poly_clipRt[0][0]) {
        win_left = DSP4_vars.poly_clipRt[0][0];
    }
    if (win_right < DSP4_vars.poly_clipLf[0][1]) {
        win_right = DSP4_vars.poly_clipLf[0][1];
    }
    if (win_right > DSP4_vars.poly_clipRt[0][1]) {
        win_right = DSP4_vars.poly_clipRt[0][1];
    }

    // SR = 0x80

    // initial output for polygon #1
    DSP4_CLEAR_OUT();
    DSP4_WRITE_BYTE(win_left & 0xff);
    DSP4_WRITE_BYTE(win_right & 0xff);

    do {
        int16_t polygon;
        ////////////////////////////////////////////////////
        // command check

        // scan next command
        DSP4.in_count = 2;
        DSP4_WAIT(1)
    resume1:

        // terminate op
        DSP4_vars.distance
            = DSP4_READ_WORD();
        if (DSP4_vars.distance == -0x8000)
            break;

        // already have 2 bytes in queue
        DSP4.in_count = 16;

        DSP4_WAIT(2)
    resume2:

        // look at guidelines for both polygon shapes
        view_x[0]
            = DSP4_READ_WORD();
        view_y[0] = DSP4_READ_WORD();
        view_x[1] = DSP4_READ_WORD();
        view_y[1] = DSP4_READ_WORD();

        // envelope shaping guidelines (one frame only)
        envelope[0][0] = DSP4_READ_WORD();
        envelope[0][1] = DSP4_READ_WORD();
        envelope[1][0] = DSP4_READ_WORD();
        envelope[1][1] = DSP4_READ_WORD();

        ////////////////////////////////////////////////////
        // projection begins

        // init
        DSP4_CLEAR_OUT();

        //////////////////////////////////////////////
        // solid polygon renderer - 2 shapes

        for (polygon = 0; polygon < 2; polygon++) {
            int32_t left_inc, right_inc;
            int16_t x1_final, x2_final;
            int16_t env[2][2];
            int16_t poly;

            // SR = 0x00

            // # DSP4_vars.raster lines to draw
            DSP4_vars.segments = DSP4_vars.poly_raster[polygon][0] - view_y[polygon];

            // prevent overdraw
            if (DSP4_vars.segments > 0) {
                // bump drawing cursor
                DSP4_vars.poly_raster[polygon][0] = view_y[polygon];
                DSP4_vars.poly_raster[polygon][1] = view_y[polygon];
            } else
                DSP4_vars.segments = 0;

            // don't draw outside the window
            if (view_y[polygon] < DSP4_vars.poly_top[polygon][0]) {
                DSP4_vars.segments = 0;

                // flush remaining DSP4_vars.raster lines
                if (view_y[polygon] >= DSP4_vars.poly_top[polygon][0])
                    DSP4_vars.segments = view_y[polygon] - DSP4_vars.poly_top[polygon][0];
            }

            // SR = 0x80

            // tell user how many DSP4_vars.raster structures to read in
            DSP4_WRITE_WORD(DSP4_vars.segments);

            // normal parameters
            poly = polygon;

            /////////////////////////////////////////////////////

            // scan next command if no SR check needed
            if (DSP4_vars.segments) {
                int32_t win_left, win_right;

                // road turnoff selection
                if ((uint16_t)envelope[polygon][0] == (uint16_t)0xc001)
                    poly = 1;
                else if (envelope[polygon][1] == 0x3fff)
                    poly = 1;

                ///////////////////////////////////////////////
                // left side of polygon

                // perspective correction on additional shaping parameters
                env[0][0] = envelope[polygon][0] * DSP4_vars.poly_plane[poly] >> 15;
                env[0][1] = envelope[polygon][0] * DSP4_vars.distance >> 15;

                // project new shapes (left side)
                x1_final = view_x[poly] + env[0][0];
                x2_final = DSP4_vars.poly_start[poly] + env[0][1];

                // interpolate between projected points with shaping
                left_inc = (x2_final - x1_final) * DSP4_Inverse(DSP4_vars.segments) << 1;
                if (DSP4_vars.segments == 1)
                    left_inc = -left_inc;

                ///////////////////////////////////////////////
                // right side of polygon

                // perspective correction on additional shaping parameters
                env[1][0] = envelope[polygon][1] * DSP4_vars.poly_plane[poly] >> 15;
                ;
                env[1][1] = envelope[polygon][1] * DSP4_vars.distance >> 15;

                // project new shapes (right side)
                x1_final = view_x[poly] + env[1][0];
                x2_final = DSP4_vars.poly_start[poly] + env[1][1];

                // interpolate between projected points with shaping
                right_inc = (x2_final - x1_final) * DSP4_Inverse(DSP4_vars.segments) << 1;
                if (DSP4_vars.segments == 1)
                    right_inc = -right_inc;

                ///////////////////////////////////////////////
                // update each point on the line

                win_left = SEX16(DSP4_vars.poly_cx[polygon][0] - DSP4_vars.poly_start[poly] + env[0][0]);
                win_right = SEX16(DSP4_vars.poly_cx[polygon][1] - DSP4_vars.poly_start[poly] + env[1][0]);

                // update DSP4_vars.distance drawn into world
                DSP4_vars.poly_plane[polygon] = DSP4_vars.distance;

                // rasterize line
                for (DSP4_vars.lcv = 0; DSP4_vars.lcv < DSP4_vars.segments; DSP4_vars.lcv++) {
                    int16_t x_left, x_right;

                    // project new coordinates
                    win_left += left_inc;
                    win_right += right_inc;

                    // grab integer portion, drop fraction (no rounding)
                    x_left = (int16_t)(win_left >> 16);
                    x_right = (int16_t)(win_right >> 16);

                    // saturate offscreen data
                    if (x_left < DSP4_vars.poly_clipLf[polygon][0])
                        x_left = DSP4_vars.poly_clipLf[polygon][0];
                    if (x_left > DSP4_vars.poly_clipRt[polygon][0])
                        x_left = DSP4_vars.poly_clipRt[polygon][0];
                    if (x_right < DSP4_vars.poly_clipLf[polygon][1])
                        x_right = DSP4_vars.poly_clipLf[polygon][1];
                    if (x_right > DSP4_vars.poly_clipRt[polygon][1])
                        x_right = DSP4_vars.poly_clipRt[polygon][1];

                    // 1. HDMA memory pointer
                    // 2. Left window position ($2126/$2128)
                    // 3. Right window position ($2127/$2129)

                    DSP4_WRITE_WORD(DSP4_vars.poly_ptr[polygon][0]);
                    DSP4_WRITE_BYTE(x_left & 0xff);
                    DSP4_WRITE_BYTE(x_right & 0xff);

                    // update memory pointers
                    DSP4_vars.poly_ptr[polygon][0] -= 4;
                    DSP4_vars.poly_ptr[polygon][1] -= 4;
                } // end rasterize line
            }

            ////////////////////////////////////////////////
            // Post-update

            // new projection spot to continue rasterizing from
            DSP4_vars.poly_start[polygon] = view_x[poly];
        } // end polygon rasterizer
    } while (1);

    // unknown output
    DSP4_CLEAR_OUT();
    DSP4_WRITE_WORD(0);

    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

void DSP4_OP09()
{
    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    case 3:
        goto resume3;
        break;
    case 4:
        goto resume4;
        break;
    case 5:
        goto resume5;
        break;
    case 6:
        goto resume6;
        break;
    }

    ////////////////////////////////////////////////////
    // process initial inputs

    // grab screen information
    DSP4_vars.viewport_cx = DSP4_READ_WORD();
    DSP4_vars.viewport_cy = DSP4_READ_WORD();
    DSP4_READ_WORD(); // 0x0000
    DSP4_vars.viewport_left = DSP4_READ_WORD();
    DSP4_vars.viewport_right = DSP4_READ_WORD();
    DSP4_vars.viewport_top = DSP4_READ_WORD();
    DSP4_vars.viewport_bottom = DSP4_READ_WORD();

    // starting DSP4_vars.raster line below the horizon
    DSP4_vars.poly_bottom[0][0] = DSP4_vars.viewport_bottom - DSP4_vars.viewport_cy;
    DSP4_vars.poly_raster[0][0] = 0x100;

    do {
        ////////////////////////////////////////////////////
        // check for new sprites

        DSP4.in_count = 4;
        DSP4_WAIT(1)
    resume1:

        ////////////////////////////////////////////////
        // DSP4_vars.raster overdraw check

        DSP4_vars.raster
            = DSP4_READ_WORD();

        // continue updating the DSP4_vars.raster line where overdraw begins
        if (DSP4_vars.raster < DSP4_vars.poly_raster[0][0]) {
            DSP4_vars.sprite_clipy = DSP4_vars.viewport_bottom - (DSP4_vars.poly_bottom[0][0] - DSP4_vars.raster);
            DSP4_vars.poly_raster[0][0] = DSP4_vars.raster;
        }

        /////////////////////////////////////////////////
        // identify sprite

        // op termination
        DSP4_vars.distance = DSP4_READ_WORD();
        if (DSP4_vars.distance == -0x8000)
            goto terminate;

        // no sprite
        if (DSP4_vars.distance == 0x0000) {
            continue;
        }

        ////////////////////////////////////////////////////
        // process projection information

        // vehicle sprite
        if ((uint16_t)DSP4_vars.distance == 0x9000) {
            int16_t car_left, car_right, car_back;
            int16_t impact_left, impact_back;
            int16_t world_spx, world_spy;
            int16_t view_spx, view_spy;
            uint16_t energy;

            // we already have 4 bytes we want
            DSP4.in_count = 14;
            DSP4_WAIT(2)
        resume2:

            // filter inputs
            energy
                = DSP4_READ_WORD();
            impact_back = DSP4_READ_WORD();
            car_back = DSP4_READ_WORD();
            impact_left = DSP4_READ_WORD();
            car_left = DSP4_READ_WORD();
            DSP4_vars.distance = DSP4_READ_WORD();
            car_right = DSP4_READ_WORD();

            // calculate car's world (x,y) values
            world_spx = car_right - car_left;
            world_spy = car_back;

            // add in collision vector [needs bit-twiddling]
            world_spx -= energy * (impact_left - car_left) >> 16;
            world_spy -= energy * (car_back - impact_back) >> 16;

            // perspective correction for world (x,y)
            view_spx = world_spx * DSP4_vars.distance >> 15;
            view_spy = world_spy * DSP4_vars.distance >> 15;

            // convert to screen values
            DSP4_vars.sprite_x = DSP4_vars.viewport_cx + view_spx;
            DSP4_vars.sprite_y = DSP4_vars.viewport_bottom - (DSP4_vars.poly_bottom[0][0] - view_spy);

            // make the car's (x)-coordinate available
            DSP4_CLEAR_OUT();
            DSP4_WRITE_WORD(world_spx);

            // grab a few remaining vehicle values
            DSP4.in_count = 4;
            DSP4_WAIT(3)
        resume3:

            // add vertical lift factor
            DSP4_vars.sprite_y
                += DSP4_READ_WORD();
        }
        // terrain sprite
        else {
            int16_t world_spx, world_spy;
            int16_t view_spx, view_spy;

            // we already have 4 bytes we want
            DSP4.in_count = 10;
            DSP4_WAIT(4)
        resume4:

            // sort loop inputs
            DSP4_vars.poly_cx[0][0]
                = DSP4_READ_WORD();
            DSP4_vars.poly_raster[0][1] = DSP4_READ_WORD();
            world_spx = DSP4_READ_WORD();
            world_spy = DSP4_READ_WORD();

            // compute base DSP4_vars.raster line from the bottom
            DSP4_vars.segments = DSP4_vars.poly_bottom[0][0] - DSP4_vars.raster;

            // perspective correction for world (x,y)
            view_spx = world_spx * DSP4_vars.distance >> 15;
            view_spy = world_spy * DSP4_vars.distance >> 15;

            // convert to screen values
            DSP4_vars.sprite_x = DSP4_vars.viewport_cx + view_spx - DSP4_vars.poly_cx[0][0];
            DSP4_vars.sprite_y = DSP4_vars.viewport_bottom - DSP4_vars.segments + view_spy;
        }

        // default sprite size: 16x16
        DSP4_vars.sprite_size = 1;
        DSP4_vars.sprite_attr = DSP4_READ_WORD();

        ////////////////////////////////////////////////////
        // convert tile data to SNES OAM format

        do {
            uint16_t header;

            int16_t sp_x, sp_y, sp_attr, sp_dattr;
            int16_t sp_dx, sp_dy;
            int16_t pixels;

            bool draw;

            DSP4.in_count = 2;
            DSP4_WAIT(5)
        resume5:

            draw
                = true;

            // opcode termination
            DSP4_vars.raster = DSP4_READ_WORD();
            if (DSP4_vars.raster == -0x8000)
                goto terminate;

            // stop code
            if (DSP4_vars.raster == 0x0000 && !DSP4_vars.sprite_size)
                break;

            // toggle sprite size
            if (DSP4_vars.raster == 0x0000) {
                DSP4_vars.sprite_size = !DSP4_vars.sprite_size;
                continue;
            }

            // check for valid sprite header
            header = DSP4_vars.raster;
            header >>= 8;
            if (header != 0x20 && header != 0x2e && // This is for attractor sprite
                header != 0x40 && header != 0x60 && header != 0xa0 && header != 0xc0 && header != 0xe0)
                break;

            // read in rest of sprite data
            DSP4.in_count = 4;
            DSP4_WAIT(6)
        resume6:

            draw
                = true;

            /////////////////////////////////////
            // process tile data

            // sprite deltas
            sp_dattr = DSP4_vars.raster;
            sp_dy = DSP4_READ_WORD();
            sp_dx = DSP4_READ_WORD();

            // update coordinates to screen space
            sp_x = DSP4_vars.sprite_x + sp_dx;
            sp_y = DSP4_vars.sprite_y + sp_dy;

            // update sprite nametable/attribute information
            sp_attr = DSP4_vars.sprite_attr + sp_dattr;

            // allow partially visibile tiles
            pixels = DSP4_vars.sprite_size ? 15 : 7;

            DSP4_CLEAR_OUT();

            // transparent tile to clip off parts of a sprite (overdraw)
            if (DSP4_vars.sprite_clipy - pixels <= sp_y && sp_y <= DSP4_vars.sprite_clipy && sp_x >= DSP4_vars.viewport_left - pixels && sp_x <= DSP4_vars.viewport_right && DSP4_vars.sprite_clipy >= DSP4_vars.viewport_top - pixels && DSP4_vars.sprite_clipy <= DSP4_vars.viewport_bottom) {
                DSP4_OP0B(&draw, sp_x, DSP4_vars.sprite_clipy, 0x00EE, DSP4_vars.sprite_size, 0);
            }

            // normal sprite tile
            if (sp_x >= DSP4_vars.viewport_left - pixels && sp_x <= DSP4_vars.viewport_right && sp_y >= DSP4_vars.viewport_top - pixels && sp_y <= DSP4_vars.viewport_bottom && sp_y <= DSP4_vars.sprite_clipy) {
                DSP4_OP0B(&draw, sp_x, sp_y, sp_attr, DSP4_vars.sprite_size, 0);
            }

            // no following OAM data
            DSP4_OP0B(&draw, 0, 0x0100, 0, 0, 1);
        } while (1);
    } while (1);

terminate:
    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

const uint16_t OP0A_Values[16] = { 0x0000, 0x0030, 0x0060, 0x0090, 0x00c0, 0x00f0, 0x0120, 0x0150, 0xfe80,
    0xfeb0, 0xfee0, 0xff10, 0xff40, 0xff70, 0xffa0, 0xffd0 };

void DSP4_OP0A(int16_t n2, int16_t* o1, int16_t* o2, int16_t* o3, int16_t* o4)
{
    *o4 = OP0A_Values[(n2 & 0x000f)];
    *o3 = OP0A_Values[(n2 & 0x00f0) >> 4];
    *o2 = OP0A_Values[(n2 & 0x0f00) >> 8];
    *o1 = OP0A_Values[(n2 & 0xf000) >> 12];
}

//////////////////////////////////////////////////////////////

void DSP4_OP0B(bool* draw, int16_t sp_x, int16_t sp_y, int16_t sp_attr, bool size, bool stop)
{
    int16_t Row1, Row2;

    // SR = 0x00

    // align to nearest 8-pixel row
    Row1 = (sp_y >> 3) & 0x1f;
    Row2 = (Row1 + 1) & 0x1f;

    // check boundaries
    if (!((sp_y < 0) || ((sp_y & 0x01ff) < 0x00eb))) {
        *draw = 0;
    }
    if (size) {
        if (DSP4_vars.OAM_Row[Row1] + 1 >= DSP4_vars.OAM_RowMax)
            *draw = 0;
        if (DSP4_vars.OAM_Row[Row2] + 1 >= DSP4_vars.OAM_RowMax)
            *draw = 0;
    } else {
        if (DSP4_vars.OAM_Row[Row1] >= DSP4_vars.OAM_RowMax) {
            *draw = 0;
        }
    }

    // emulator fail-safe (unknown if this really exists)
    if (DSP4_vars.sprite_count >= 128) {
        *draw = 0;
    }

    // SR = 0x80

    if (*draw) {
        // Row tiles
        if (size) {
            DSP4_vars.OAM_Row[Row1] += 2;
            DSP4_vars.OAM_Row[Row2] += 2;
        } else {
            DSP4_vars.OAM_Row[Row1]++;
        }

        // yield OAM output
        DSP4_WRITE_WORD(1);

        // pack OAM data: x,y,name,attr
        DSP4_WRITE_BYTE(sp_x & 0xff);
        DSP4_WRITE_BYTE(sp_y & 0xff);
        DSP4_WRITE_WORD(sp_attr);

        DSP4_vars.sprite_count++;

        // OAM: size,msb data
        // save post-oam table data for future retrieval
        DSP4_vars.OAM_attr[DSP4_vars.OAM_index] |= ((sp_x < 0 || sp_x > 255) << DSP4_vars.OAM_bits);
        DSP4_vars.OAM_bits++;

        DSP4_vars.OAM_attr[DSP4_vars.OAM_index] |= (size << DSP4_vars.OAM_bits);
        DSP4_vars.OAM_bits++;

        // move to next byte in buffer
        if (DSP4_vars.OAM_bits == 16) {
            DSP4_vars.OAM_bits = 0;
            DSP4_vars.OAM_index++;
        }
    } else if (stop) {
        // yield no OAM output
        DSP4_WRITE_WORD(0);
    }
}

//////////////////////////////////////////////////////////////

void DSP4_OP0D()
{
    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    }

    ////////////////////////////////////////////////////
    // process initial inputs

    // sort inputs
    DSP4_vars.world_y = DSP4_READ_DWORD();
    DSP4_vars.poly_bottom[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][0] = DSP4_READ_WORD();
    DSP4_vars.viewport_bottom = DSP4_READ_WORD();
    DSP4_vars.world_x = DSP4_READ_DWORD();
    DSP4_vars.poly_cx[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[0][0] = DSP4_READ_WORD();
    DSP4_vars.world_yofs = DSP4_READ_WORD();
    DSP4_vars.world_dy = DSP4_READ_DWORD();
    DSP4_vars.world_dx = DSP4_READ_DWORD();
    DSP4_vars.distance = DSP4_READ_WORD();
    DSP4_READ_WORD(); // 0x0000
    DSP4_vars.world_xenv = SEX78(DSP4_READ_WORD());
    DSP4_vars.world_ddy = DSP4_READ_WORD();
    DSP4_vars.world_ddx = DSP4_READ_WORD();
    DSP4_vars.view_yofsenv = DSP4_READ_WORD();

    // initial (x,y,offset) at starting DSP4_vars.raster line
    DSP4_vars.view_x1 = (int16_t)((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16);
    DSP4_vars.view_y1 = (int16_t)(DSP4_vars.world_y >> 16);
    DSP4_vars.view_xofs1 = (int16_t)(DSP4_vars.world_x >> 16);
    DSP4_vars.view_yofs1 = DSP4_vars.world_yofs;

    // first DSP4_vars.raster line
    DSP4_vars.poly_raster[0][0] = DSP4_vars.poly_bottom[0][0];

    do {
        ////////////////////////////////////////////////////
        // process one iteration of projection

        // perspective projection of world (x,y,scroll) points
        // based on the current projection lines
        DSP4_vars.view_x2 = (int16_t)((((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16) * DSP4_vars.distance >> 15) + (DSP4_vars.view_turnoff_x * DSP4_vars.distance >> 15));
        DSP4_vars.view_y2 = (int16_t)((DSP4_vars.world_y >> 16) * DSP4_vars.distance >> 15);
        DSP4_vars.view_xofs2 = DSP4_vars.view_x2;
        DSP4_vars.view_yofs2 = (DSP4_vars.world_yofs * DSP4_vars.distance >> 15) + DSP4_vars.poly_bottom[0][0] - DSP4_vars.view_y2;

        // 1. World x-location before transformation
        // 2. Viewer x-position at the current
        // 3. World y-location before perspective projection
        // 4. Viewer y-position below the horizon
        // 5. Number of DSP4_vars.raster lines drawn in this iteration

        DSP4_CLEAR_OUT();
        DSP4_WRITE_WORD((uint16_t)((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16));
        DSP4_WRITE_WORD(DSP4_vars.view_x2);
        DSP4_WRITE_WORD((uint16_t)(DSP4_vars.world_y >> 16));
        DSP4_WRITE_WORD(DSP4_vars.view_y2);

        //////////////////////////////////////////////////////////

        // SR = 0x00

        // determine # of DSP4_vars.raster lines used
        DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.view_y2;

        // prevent overdraw
        if (DSP4_vars.view_y2 >= DSP4_vars.poly_raster[0][0])
            DSP4_vars.segments = 0;
        else
            DSP4_vars.poly_raster[0][0] = DSP4_vars.view_y2;

        // don't draw outside the window
        if (DSP4_vars.view_y2 < DSP4_vars.poly_top[0][0]) {
            DSP4_vars.segments = 0;

            // flush remaining DSP4_vars.raster lines
            if (DSP4_vars.view_y1 >= DSP4_vars.poly_top[0][0])
                DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.poly_top[0][0];
        }

        // SR = 0x80

        DSP4_WRITE_WORD(DSP4_vars.segments);

        //////////////////////////////////////////////////////////

        // scan next command if no SR check needed
        if (DSP4_vars.segments) {
            int32_t px_dx, py_dy;
            int32_t x_scroll, y_scroll;

            // SR = 0x00

            // linear interpolation (lerp) between projected points
            px_dx = (DSP4_vars.view_xofs2 - DSP4_vars.view_xofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;
            py_dy = (DSP4_vars.view_yofs2 - DSP4_vars.view_yofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;

            // starting step values
            x_scroll = SEX16(DSP4_vars.poly_cx[0][0] + DSP4_vars.view_xofs1);
            y_scroll = SEX16(-DSP4_vars.viewport_bottom + DSP4_vars.view_yofs1 + DSP4_vars.view_yofsenv + DSP4_vars.poly_cx[1][0] - DSP4_vars.world_yofs);

            // SR = 0x80

            // rasterize line
            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < DSP4_vars.segments; DSP4_vars.lcv++) {
                // 1. HDMA memory pointer (bg1)
                // 2. vertical scroll offset ($210E)
                // 3. horizontal scroll offset ($210D)

                DSP4_WRITE_WORD(DSP4_vars.poly_ptr[0][0]);
                DSP4_WRITE_WORD((uint16_t)((y_scroll + 0x8000) >> 16));
                DSP4_WRITE_WORD((uint16_t)((x_scroll + 0x8000) >> 16));

                // update memory address
                DSP4_vars.poly_ptr[0][0] -= 4;

                // update screen values
                x_scroll += px_dx;
                y_scroll += py_dy;
            }
        }

        /////////////////////////////////////////////////////
        // Post-update

        // update new viewer (x,y,scroll) to last DSP4_vars.raster line drawn
        DSP4_vars.view_x1 = DSP4_vars.view_x2;
        DSP4_vars.view_y1 = DSP4_vars.view_y2;
        DSP4_vars.view_xofs1 = DSP4_vars.view_xofs2;
        DSP4_vars.view_yofs1 = DSP4_vars.view_yofs2;

        // add deltas for projection lines
        DSP4_vars.world_dx += SEX78(DSP4_vars.world_ddx);
        DSP4_vars.world_dy += SEX78(DSP4_vars.world_ddy);

        // update projection lines
        DSP4_vars.world_x += (DSP4_vars.world_dx + DSP4_vars.world_xenv);
        DSP4_vars.world_y += DSP4_vars.world_dy;

        ////////////////////////////////////////////////////
        // command check

        // scan next command
        DSP4.in_count = 2;
        DSP4_WAIT(1)
    resume1:

        // inspect input
        DSP4_vars.distance
            = DSP4_READ_WORD();

        // terminate op
        if (DSP4_vars.distance == -0x8000)
            break;

        // already have 2 bytes in queue
        DSP4.in_count = 6;
        DSP4_WAIT(2)
    resume2:

        // inspect inputs
        DSP4_vars.world_ddy
            = DSP4_READ_WORD();
        DSP4_vars.world_ddx = DSP4_READ_WORD();
        DSP4_vars.view_yofsenv = DSP4_READ_WORD();

        // no envelope here
        DSP4_vars.world_xenv = 0;
    } while (1);

    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

void DSP4_OP0E()
{
    DSP4_vars.OAM_RowMax = 16;
    memset(DSP4_vars.OAM_Row, 0, 64);
}

//////////////////////////////////////////////////////////////

void DSP4_OP0F()
{
    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    case 3:
        goto resume3;
        break;
    case 4:
        goto resume4;
        break;
    }

    ////////////////////////////////////////////////////
    // process initial inputs

    // sort inputs
    DSP4_READ_WORD(); // 0x0000
    DSP4_vars.world_y = DSP4_READ_DWORD();
    DSP4_vars.poly_bottom[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][0] = DSP4_READ_WORD();
    DSP4_vars.viewport_bottom = DSP4_READ_WORD();
    DSP4_vars.world_x = DSP4_READ_DWORD();
    DSP4_vars.poly_cx[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[0][0] = DSP4_READ_WORD();
    DSP4_vars.world_yofs = DSP4_READ_WORD();
    DSP4_vars.world_dy = DSP4_READ_DWORD();
    DSP4_vars.world_dx = DSP4_READ_DWORD();
    DSP4_vars.distance = DSP4_READ_WORD();
    DSP4_READ_WORD(); // 0x0000
    DSP4_vars.world_xenv = DSP4_READ_DWORD();
    DSP4_vars.world_ddy = DSP4_READ_WORD();
    DSP4_vars.world_ddx = DSP4_READ_WORD();
    DSP4_vars.view_yofsenv = DSP4_READ_WORD();

    // initial (x,y,offset) at starting DSP4_vars.raster line
    DSP4_vars.view_x1 = (int16_t)((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16);
    DSP4_vars.view_y1 = (int16_t)(DSP4_vars.world_y >> 16);
    DSP4_vars.view_xofs1 = (int16_t)(DSP4_vars.world_x >> 16);
    DSP4_vars.view_yofs1 = DSP4_vars.world_yofs;
    DSP4_vars.view_turnoff_x = 0;
    DSP4_vars.view_turnoff_dx = 0;

    // first DSP4_vars.raster line
    DSP4_vars.poly_raster[0][0] = DSP4_vars.poly_bottom[0][0];

    do {
        ////////////////////////////////////////////////////
        // process one iteration of projection

        // perspective projection of world (x,y,scroll) points
        // based on the current projection lines
        DSP4_vars.view_x2 = (int16_t)(((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16) * DSP4_vars.distance >> 15);
        DSP4_vars.view_y2 = (int16_t)((DSP4_vars.world_y >> 16) * DSP4_vars.distance >> 15);
        DSP4_vars.view_xofs2 = DSP4_vars.view_x2;
        DSP4_vars.view_yofs2 = (DSP4_vars.world_yofs * DSP4_vars.distance >> 15) + DSP4_vars.poly_bottom[0][0] - DSP4_vars.view_y2;

        // 1. World x-location before transformation
        // 2. Viewer x-position at the next
        // 3. World y-location before perspective projection
        // 4. Viewer y-position below the horizon
        // 5. Number of DSP4_vars.raster lines drawn in this iteration

        DSP4_CLEAR_OUT();
        DSP4_WRITE_WORD((uint16_t)((DSP4_vars.world_x + DSP4_vars.world_xenv) >> 16));
        DSP4_WRITE_WORD(DSP4_vars.view_x2);
        DSP4_WRITE_WORD((uint16_t)(DSP4_vars.world_y >> 16));
        DSP4_WRITE_WORD(DSP4_vars.view_y2);

        //////////////////////////////////////////////////////

        // SR = 0x00

        // determine # of DSP4_vars.raster lines used
        DSP4_vars.segments = DSP4_vars.poly_raster[0][0] - DSP4_vars.view_y2;

        // prevent overdraw
        if (DSP4_vars.view_y2 >= DSP4_vars.poly_raster[0][0])
            DSP4_vars.segments = 0;
        else
            DSP4_vars.poly_raster[0][0] = DSP4_vars.view_y2;

        // don't draw outside the window
        if (DSP4_vars.view_y2 < DSP4_vars.poly_top[0][0]) {
            DSP4_vars.segments = 0;

            // flush remaining DSP4_vars.raster lines
            if (DSP4_vars.view_y1 >= DSP4_vars.poly_top[0][0])
                DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.poly_top[0][0];
        }

        // SR = 0x80

        DSP4_WRITE_WORD(DSP4_vars.segments);

        //////////////////////////////////////////////////////

        // scan next command if no SR check needed
        if (DSP4_vars.segments) {
            int32_t px_dx, py_dy;
            int32_t x_scroll, y_scroll;

            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < 4; DSP4_vars.lcv++) {
                // grab inputs
                DSP4.in_count = 4;
                DSP4_WAIT(1);
            resume1:
                for (;;) {
                    int16_t distance;
                    int16_t color, red, green, blue;

                    distance = DSP4_READ_WORD();
                    color = DSP4_READ_WORD();

                    // U1+B5+G5+R5
                    red = color & 0x1f;
                    green = (color >> 5) & 0x1f;
                    blue = (color >> 10) & 0x1f;

                    // dynamic lighting
                    red = (red * distance >> 15) & 0x1f;
                    green = (green * distance >> 15) & 0x1f;
                    blue = (blue * distance >> 15) & 0x1f;
                    color = red | (green << 5) | (blue << 10);

                    DSP4_CLEAR_OUT();
                    DSP4_WRITE_WORD(color);
                    break;
                }
            }

            //////////////////////////////////////////////////////

            // SR = 0x00

            // linear interpolation (lerp) between projected points
            px_dx = (DSP4_vars.view_xofs2 - DSP4_vars.view_xofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;
            py_dy = (DSP4_vars.view_yofs2 - DSP4_vars.view_yofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;

            // starting step values
            x_scroll = SEX16(DSP4_vars.poly_cx[0][0] + DSP4_vars.view_xofs1);
            y_scroll = SEX16(-DSP4_vars.viewport_bottom + DSP4_vars.view_yofs1 + DSP4_vars.view_yofsenv + DSP4_vars.poly_cx[1][0] - DSP4_vars.world_yofs);

            // SR = 0x80

            // rasterize line
            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < DSP4_vars.segments; DSP4_vars.lcv++) {
                // 1. HDMA memory pointer
                // 2. vertical scroll offset ($210E)
                // 3. horizontal scroll offset ($210D)

                DSP4_WRITE_WORD(DSP4_vars.poly_ptr[0][0]);
                DSP4_WRITE_WORD((uint16_t)((y_scroll + 0x8000) >> 16));
                DSP4_WRITE_WORD((uint16_t)((x_scroll + 0x8000) >> 16));

                // update memory address
                DSP4_vars.poly_ptr[0][0] -= 4;

                // update screen values
                x_scroll += px_dx;
                y_scroll += py_dy;
            }
        }

        ////////////////////////////////////////////////////
        // Post-update

        // update new viewer (x,y,scroll) to last DSP4_vars.raster line drawn
        DSP4_vars.view_x1 = DSP4_vars.view_x2;
        DSP4_vars.view_y1 = DSP4_vars.view_y2;
        DSP4_vars.view_xofs1 = DSP4_vars.view_xofs2;
        DSP4_vars.view_yofs1 = DSP4_vars.view_yofs2;

        // add deltas for projection lines
        DSP4_vars.world_dx += SEX78(DSP4_vars.world_ddx);
        DSP4_vars.world_dy += SEX78(DSP4_vars.world_ddy);

        // update projection lines
        DSP4_vars.world_x += (DSP4_vars.world_dx + DSP4_vars.world_xenv);
        DSP4_vars.world_y += DSP4_vars.world_dy;

        // update road turnoff position
        DSP4_vars.view_turnoff_x += DSP4_vars.view_turnoff_dx;

        ////////////////////////////////////////////////////
        // command check

        // scan next command
        DSP4.in_count = 2;
        DSP4_WAIT(2)
    resume2:

        // check for termination
        DSP4_vars.distance
            = DSP4_READ_WORD();
        if (DSP4_vars.distance == -0x8000)
            break;

        // road splice
        if ((uint16_t)DSP4_vars.distance == 0x8001) {
            DSP4.in_count = 6;
            DSP4_WAIT(3)
        resume3:

            DSP4_vars.distance
                = DSP4_READ_WORD();
            DSP4_vars.view_turnoff_x = DSP4_READ_WORD();
            DSP4_vars.view_turnoff_dx = DSP4_READ_WORD();

            // factor in new changes
            DSP4_vars.view_x1 += (DSP4_vars.view_turnoff_x * DSP4_vars.distance >> 15);
            DSP4_vars.view_xofs1 += (DSP4_vars.view_turnoff_x * DSP4_vars.distance >> 15);

            // update stepping values
            DSP4_vars.view_turnoff_x += DSP4_vars.view_turnoff_dx;

            DSP4.in_count = 2;
            DSP4_WAIT(2)
        }

        // already have 2 bytes in queue
        DSP4.in_count = 6;
        DSP4_WAIT(4)
    resume4:

        // inspect inputs
        DSP4_vars.world_ddy
            = DSP4_READ_WORD();
        DSP4_vars.world_ddx = DSP4_READ_WORD();
        DSP4_vars.view_yofsenv = DSP4_READ_WORD();

        // no envelope here
        DSP4_vars.world_xenv = 0;
    } while (1);

    // terminate op
    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

void DSP4_OP10()
{
    DSP4.waiting4command = false;

    // op flow control
    switch (DSP4_vars.DSP4_Logic) {
    case 1:
        goto resume1;
        break;
    case 2:
        goto resume2;
        break;
    case 3:
        goto resume3;
        break;
    }

    ////////////////////////////////////////////////////
    // sort inputs

    DSP4_READ_WORD(); // 0x0000
    DSP4_vars.world_y = DSP4_READ_DWORD();
    DSP4_vars.poly_bottom[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_top[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_cx[1][0] = DSP4_READ_WORD();
    DSP4_vars.viewport_bottom = DSP4_READ_WORD();
    DSP4_vars.world_x = DSP4_READ_DWORD();
    DSP4_vars.poly_cx[0][0] = DSP4_READ_WORD();
    DSP4_vars.poly_ptr[0][0] = DSP4_READ_WORD();
    DSP4_vars.world_yofs = DSP4_READ_WORD();
    DSP4_vars.distance = DSP4_READ_WORD();
    DSP4_vars.view_y2 = DSP4_READ_WORD();
    DSP4_vars.view_dy = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
    DSP4_vars.view_x2 = DSP4_READ_WORD();
    DSP4_vars.view_dx = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
    DSP4_vars.view_yofsenv = DSP4_READ_WORD();

    // initial (x,y,offset) at starting DSP4_vars.raster line
    DSP4_vars.view_x1 = (int16_t)(DSP4_vars.world_x >> 16);
    DSP4_vars.view_y1 = (int16_t)(DSP4_vars.world_y >> 16);
    DSP4_vars.view_xofs1 = DSP4_vars.view_x1;
    DSP4_vars.view_yofs1 = DSP4_vars.world_yofs;

    // first DSP4_vars.raster line
    DSP4_vars.poly_raster[0][0] = DSP4_vars.poly_bottom[0][0];

    do {
        ////////////////////////////////////////////////////
        // process one iteration of projection

        // add shaping
        DSP4_vars.view_x2 += DSP4_vars.view_dx;
        DSP4_vars.view_y2 += DSP4_vars.view_dy;

        // vertical scroll calculation
        DSP4_vars.view_xofs2 = DSP4_vars.view_x2;
        DSP4_vars.view_yofs2 = (DSP4_vars.world_yofs * DSP4_vars.distance >> 15) + DSP4_vars.poly_bottom[0][0] - DSP4_vars.view_y2;

        // 1. Viewer x-position at the next
        // 2. Viewer y-position below the horizon
        // 3. Number of DSP4_vars.raster lines drawn in this iteration

        DSP4_CLEAR_OUT();
        DSP4_WRITE_WORD(DSP4_vars.view_x2);
        DSP4_WRITE_WORD(DSP4_vars.view_y2);

        //////////////////////////////////////////////////////

        // SR = 0x00

        // determine # of DSP4_vars.raster lines used
        DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.view_y2;

        // prevent overdraw
        if (DSP4_vars.view_y2 >= DSP4_vars.poly_raster[0][0])
            DSP4_vars.segments = 0;
        else
            DSP4_vars.poly_raster[0][0] = DSP4_vars.view_y2;

        // don't draw outside the window
        if (DSP4_vars.view_y2 < DSP4_vars.poly_top[0][0]) {
            DSP4_vars.segments = 0;

            // flush remaining DSP4_vars.raster lines
            if (DSP4_vars.view_y1 >= DSP4_vars.poly_top[0][0])
                DSP4_vars.segments = DSP4_vars.view_y1 - DSP4_vars.poly_top[0][0];
        }

        // SR = 0x80

        DSP4_WRITE_WORD(DSP4_vars.segments);

        //////////////////////////////////////////////////////

        // scan next command if no SR check needed
        if (DSP4_vars.segments) {
            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < 4; DSP4_vars.lcv++) {
                // grab inputs
                DSP4.in_count = 4;
                DSP4_WAIT(1);
            resume1:
                for (;;) {
                    int16_t distance;
                    int16_t color, red, green, blue;

                    distance = DSP4_READ_WORD();
                    color = DSP4_READ_WORD();

                    // U1+B5+G5+R5
                    red = color & 0x1f;
                    green = (color >> 5) & 0x1f;
                    blue = (color >> 10) & 0x1f;

                    // dynamic lighting
                    red = (red * distance >> 15) & 0x1f;
                    green = (green * distance >> 15) & 0x1f;
                    blue = (blue * distance >> 15) & 0x1f;
                    color = red | (green << 5) | (blue << 10);

                    DSP4_CLEAR_OUT();
                    DSP4_WRITE_WORD(color);
                    break;
                }
            }
        }

        //////////////////////////////////////////////////////

        // scan next command if no SR check needed
        if (DSP4_vars.segments) {
            int32_t px_dx, py_dy;
            int32_t x_scroll, y_scroll;

            // SR = 0x00

            // linear interpolation (lerp) between projected points
            px_dx = (DSP4_vars.view_xofs2 - DSP4_vars.view_xofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;
            py_dy = (DSP4_vars.view_yofs2 - DSP4_vars.view_yofs1) * DSP4_Inverse(DSP4_vars.segments) << 1;

            // starting step values
            x_scroll = SEX16(DSP4_vars.poly_cx[0][0] + DSP4_vars.view_xofs1);
            y_scroll = SEX16(-DSP4_vars.viewport_bottom + DSP4_vars.view_yofs1 + DSP4_vars.view_yofsenv + DSP4_vars.poly_cx[1][0] - DSP4_vars.world_yofs);

            // SR = 0x80

            // rasterize line
            for (DSP4_vars.lcv = 0; DSP4_vars.lcv < DSP4_vars.segments; DSP4_vars.lcv++) {
                // 1. HDMA memory pointer (bg2)
                // 2. vertical scroll offset ($2110)
                // 3. horizontal scroll offset ($210F)

                DSP4_WRITE_WORD(DSP4_vars.poly_ptr[0][0]);
                DSP4_WRITE_WORD((uint16_t)((y_scroll + 0x8000) >> 16));
                DSP4_WRITE_WORD((uint16_t)((x_scroll + 0x8000) >> 16));

                // update memory address
                DSP4_vars.poly_ptr[0][0] -= 4;

                // update screen values
                x_scroll += px_dx;
                y_scroll += py_dy;
            }
        }

        /////////////////////////////////////////////////////
        // Post-update

        // update new viewer (x,y,scroll) to last DSP4_vars.raster line drawn
        DSP4_vars.view_x1 = DSP4_vars.view_x2;
        DSP4_vars.view_y1 = DSP4_vars.view_y2;
        DSP4_vars.view_xofs1 = DSP4_vars.view_xofs2;
        DSP4_vars.view_yofs1 = DSP4_vars.view_yofs2;

        ////////////////////////////////////////////////////
        // command check

        // scan next command
        DSP4.in_count = 2;
        DSP4_WAIT(2)
    resume2:

        // check for opcode termination
        DSP4_vars.distance
            = DSP4_READ_WORD();
        if (DSP4_vars.distance == -0x8000)
            break;

        // already have 2 bytes in queue
        DSP4.in_count = 10;
        DSP4_WAIT(3)
    resume3:

        // inspect inputs
        DSP4_vars.view_y2
            = DSP4_READ_WORD();
        DSP4_vars.view_dy = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
        DSP4_vars.view_x2 = DSP4_READ_WORD();
        DSP4_vars.view_dx = DSP4_READ_WORD() * DSP4_vars.distance >> 15;
    } while (1);

    DSP4.waiting4command = true;
}

//////////////////////////////////////////////////////////////

void DSP4_OP11(int16_t A, int16_t B, int16_t C, int16_t D, int16_t* M)
{
    // 0x155 = 341 = Horizontal Width of the Screen
    *M = ((A * 0x0155 >> 2) & 0xf000) | ((B * 0x0155 >> 6) & 0x0f00) | ((C * 0x0155 >> 10) & 0x00f0) | ((D * 0x0155 >> 14) & 0x000f);
}

/////////////////////////////////////////////////////////////
// Processing Code
/////////////////////////////////////////////////////////////
uint8_t dsp4_byte;
uint16_t dsp4_address;

void InitDSP4()
{
    memset(&DSP4, 0, sizeof(DSP4));
    DSP4.waiting4command = true;
}

void DSP4SetByte()
{
    // clear pending read
    if (DSP4.out_index < DSP4.out_count) {
        DSP4.out_index++;
        return;
    }

    if (DSP4.waiting4command) {
        if (DSP4.half_command) {
            DSP4.command |= (dsp4_byte << 8);
            DSP4.in_index = 0;
            DSP4.waiting4command = false;
            DSP4.half_command = false;
            DSP4.out_count = 0;
            DSP4.out_index = 0;

            DSP4_vars.DSP4_Logic = 0;

            switch (DSP4.command) {
            case 0x0000:
                DSP4.in_count = 4;
                break;
            case 0x0001:
                DSP4.in_count = 44;
                break;
            case 0x0003:
                DSP4.in_count = 0;
                break;
            case 0x0005:
                DSP4.in_count = 0;
                break;
            case 0x0006:
                DSP4.in_count = 0;
                break;
            case 0x0007:
                DSP4.in_count = 34;
                break;
            case 0x0008:
                DSP4.in_count = 90;
                break;
            case 0x0009:
                DSP4.in_count = 14;
                break;
            case 0x000a:
                DSP4.in_count = 6;
                break;
            case 0x000b:
                DSP4.in_count = 6;
                break;
            case 0x000d:
                DSP4.in_count = 42;
                break;
            case 0x000e:
                DSP4.in_count = 0;
                break;
            case 0x000f:
                DSP4.in_count = 46;
                break;
            case 0x0010:
                DSP4.in_count = 36;
                break;
            case 0x0011:
                DSP4.in_count = 8;
                break;
            default:
                DSP4.waiting4command = true;
                break;
            }
        } else {
            DSP4.command = dsp4_byte;
            DSP4.half_command = true;
        }
    } else {
        DSP4.parameters[DSP4.in_index] = dsp4_byte;
        DSP4.in_index++;
    }

    if (!DSP4.waiting4command && DSP4.in_count == DSP4.in_index) {
        // Actually execute the command
        DSP4.waiting4command = true;
        DSP4.out_index = 0;
        DSP4.in_index = 0;

        switch (DSP4.command) {
            // 16-bit multiplication
        case 0x0000: {
            int16_t multiplier, multiplicand;
            int32_t product;

            multiplier = DSP4_READ_WORD();
            multiplicand = DSP4_READ_WORD();

            DSP4_Multiply(multiplicand, multiplier, &product);

            DSP4_CLEAR_OUT();
            DSP4_WRITE_WORD((uint16_t)(product));
            DSP4_WRITE_WORD((uint16_t)(product >> 16));
        } break;

        // single-player track projection
        case 0x0001:
            DSP4_OP01();
            break;

        // single-player selection
        case 0x0003:
            DSP4_OP03();
            break;

        // clear OAM
        case 0x0005:
            DSP4_OP05();
            break;

        // transfer OAM
        case 0x0006:
            DSP4_OP06();
            break;

        // single-player track turnoff projection
        case 0x0007:
            DSP4_OP07();
            break;

        // solid polygon projection
        case 0x0008:
            DSP4_OP08();
            break;

        // sprite projection
        case 0x0009:
            DSP4_OP09();
            break;

        // unknown
        case 0x000A: {
            /*int16_t in1a = */ DSP4_READ_WORD();
            int16_t in2a = DSP4_READ_WORD();
            /*int16_t in3a = */ DSP4_READ_WORD();
            int16_t out1a, out2a, out3a, out4a;

            DSP4_OP0A(in2a, &out2a, &out1a, &out4a, &out3a);

            DSP4_CLEAR_OUT();
            DSP4_WRITE_WORD(out1a);
            DSP4_WRITE_WORD(out2a);
            DSP4_WRITE_WORD(out3a);
            DSP4_WRITE_WORD(out4a);
        } break;

        // set OAM
        case 0x000B: {
            int16_t sp_x = DSP4_READ_WORD();
            int16_t sp_y = DSP4_READ_WORD();
            int16_t sp_attr = DSP4_READ_WORD();
            bool draw = 1;

            DSP4_CLEAR_OUT();

            DSP4_OP0B(&draw, sp_x, sp_y, sp_attr, 0, 1);
        } break;

        // multi-player track projection
        case 0x000D:
            DSP4_OP0D();
            break;

        // multi-player selection
        case 0x000E:
            DSP4_OP0E();
            break;

        // single-player track projection with lighting
        case 0x000F:
            DSP4_OP0F();
            break;

        // single-player track turnoff projection with lighting
        case 0x0010:
            DSP4_OP10();
            break;

        // unknown: horizontal mapping command
        case 0x0011: {
            int16_t a, b, c, d, m;

            d = DSP4_READ_WORD();
            c = DSP4_READ_WORD();
            b = DSP4_READ_WORD();
            a = DSP4_READ_WORD();

            DSP4_OP11(a, b, c, d, &m);

            DSP4_CLEAR_OUT();
            DSP4_WRITE_WORD(m);

            break;
        }

        default:
            break;
        }
    }
}

void DSP4GetByte()
{
    if (DSP4.out_count) {
        dsp4_byte = (uint8_t)DSP4.output[DSP4.out_index & 0x1FF];
        DSP4.out_index++;
        if (DSP4.out_count == DSP4.out_index)
            DSP4.out_count = 0;
    } else {
        dsp4_byte = 0xff;
    }
}
