/*
 * bsnes-jg - Super Nintendo emulator
 *
 * Copyright (C) 2004-2006 Dreamer Nom, John Weidman, Kris Bleakley, Nach,
 *                         z80 gaiden
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, specifically version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see <https://www.gnu.org/licenses/>.
 */
/*
Due recognition and credit are given on Overload's DSP website.
Thank those contributors for their hard work on this chip.
http://users.tpg.com.au/advlink/dsp/

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')
*/

#include <cstdint>
#include <cstring>

#include "dsp4emu.hpp"

namespace SuperFamicom {

inline uint16_t READ_WORD(uint8_t *addr) {
  return (addr[0]) + (addr[1] << 8);
}

inline uint32_t READ_DWORD(uint8_t *addr) {
  return (addr[0]) + (addr[1] << 8) + (addr[2] << 16) + (addr[3] << 24);
}

inline void WRITE_WORD(uint8_t *addr, uint16_t data) {
  addr[0] = data;
  addr[1] = data >> 8;
}

static struct DSP4_t DSP4;
static 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,
};

static 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(uint8_t *draw, int16_t sp_x, int16_t sp_y, int16_t sp_attr, uint8_t size, uint8_t stop);

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

static 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;
}

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

static 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;
}

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

static 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;
}

static 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 wleft, wright;

        // 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

        wleft = SEX16(DSP4_vars.poly_cx[polygon][0] - DSP4_vars.poly_start[poly] + env[0][0]);
        wright = 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
          wleft += left_inc;
          wright += right_inc;

          // grab integer portion, drop fraction (no rounding)
          x_left = (int16_t)(wleft >> 16);
          x_right = (int16_t)(wright >> 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;
}

static 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;

      uint8_t 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
};

static 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(uint8_t *draw, int16_t sp_x, int16_t sp_y, int16_t sp_attr, uint8_t size, uint8_t 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);
  }
}

static 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;
}

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

static 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;
}

static 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;
}

static 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;

        // TODO - Reads must be performed but these values are not used?
        if (in1a || in3a) {}

        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();
        uint8_t 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;
  }
}

}
