/*****************************************************************************\
     Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
                This file is licensed under the Snes9x License.
   For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/

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


#include "snes9x.h"
#include "memmap.h"

#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) \
	{ for (int p = 0; p < 16; p++) DSP4_WRITE_WORD((d)[p]); }
#endif

// used to wait for dsp i/o
#define DSP4_WAIT(x) \
	DSP4.in_index = 0; DSP4.Logic = (x); return

// 1.7.8 -> 1.15.16
#define SEX78(a)	(((int32) ((int16) (a))) << 8)

// 1.15.0 -> 1.15.16
#define SEX16(a)	(((int32) ((int16) (a))) << 16)

static int16 DSP4_READ_WORD (void);
static int32 DSP4_READ_DWORD (void);
static int16 DSP4_Inverse (int16);
static void DSP4_Multiply (int16, int16, int32 *);
static void DSP4_OP01 (void);
static void DSP4_OP03 (void);
static void DSP4_OP05 (void);
static void DSP4_OP06 (void);
static void DSP4_OP07 (void);
static void DSP4_OP08 (void);
static void DSP4_OP09 (void);
static void DSP4_OP0A (int16, int16 *, int16 *, int16 *, int16 *);
static void DSP4_OP0B (bool8 *, int16, int16, int16, bool8, bool8);
static void DSP4_OP0D (void);
static void DSP4_OP0E (void);
static void DSP4_OP0F (void);
static void DSP4_OP10 (void);
static void DSP4_OP11 (int16, int16, int16, int16, int16 *);
static void DSP4_SetByte (void);
static void DSP4_GetByte (void);


static int16 DSP4_READ_WORD (void)
{
	int16	out;

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

	return (out);
}

static int32 DSP4_READ_DWORD (void)
{
	int32	out;

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

	return (out);
}

static int16 DSP4_Inverse (int16 value)
{
	// Attention: This lookup table is not verified
	const uint16	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
	};

	// saturate bounds
	if (value < 0)
		value = 0;
	if (value > 63)
		value = 63;

	return (div_lut[value]);
}

static void DSP4_Multiply (int16 Multiplicand, int16 Multiplier, int32 *Product)
{
	*Product = (Multiplicand * Multiplier << 1) >> 1;
}

static void DSP4_OP01 (void)
{
	DSP4.waiting4command = FALSE;

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

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

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

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

	// first raster line
	DSP4.poly_raster[0][0] = DSP4.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.view_x2    = (((DSP4.world_x + DSP4.world_xenv) >> 16) * DSP4.distance >> 15) + (DSP4.view_turnoff_x * DSP4.distance >> 15);
		DSP4.view_y2    = (DSP4.world_y >> 16) * DSP4.distance >> 15;
		DSP4.view_xofs2 = DSP4.view_x2;
		DSP4.view_yofs2 = (DSP4.world_yofs * DSP4.distance >> 15) + DSP4.poly_bottom[0][0] - DSP4.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 raster lines drawn in this iteration
		DSP4_CLEAR_OUT();
		DSP4_WRITE_WORD((DSP4.world_x + DSP4.world_xenv) >> 16);
		DSP4_WRITE_WORD(DSP4.view_x2);
		DSP4_WRITE_WORD(DSP4.world_y >> 16);
		DSP4_WRITE_WORD(DSP4.view_y2);

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

		// SR = 0x00

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

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

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

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

		// SR = 0x80

		DSP4_WRITE_WORD(DSP4.segments);

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

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

			// SR = 0x00

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

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

			// SR = 0x80

			// rasterize line
			for (DSP4.lcv = 0; DSP4.lcv < DSP4.segments; DSP4.lcv++)
			{
				// 1. HDMA memory pointer (bg1)
				// 2. vertical scroll offset ($210E)
				// 3. horizontal scroll offset ($210D)
				DSP4_WRITE_WORD(DSP4.poly_ptr[0][0]);
				DSP4_WRITE_WORD((y_scroll + 0x8000) >> 16);
				DSP4_WRITE_WORD((x_scroll + 0x8000) >> 16);

				// update memory address
				DSP4.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 raster line drawn
		DSP4.view_x1    = DSP4.view_x2;
		DSP4.view_y1    = DSP4.view_y2;
		DSP4.view_xofs1 = DSP4.view_xofs2;
		DSP4.view_yofs1 = DSP4.view_yofs2;

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

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

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

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

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

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

		// road turnoff
		if ((uint16) DSP4.distance == 0x8001)
		{
			DSP4.in_count = 6;
			DSP4_WAIT(2);

			resume2:

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

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

			// update stepping values
			DSP4.view_turnoff_x += DSP4.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.world_ddy    = DSP4_READ_WORD();
		DSP4.world_ddx    = DSP4_READ_WORD();
		DSP4.view_yofsenv = DSP4_READ_WORD();

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

	// terminate op
	DSP4.waiting4command = TRUE;
}

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

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

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

static void DSP4_OP07 (void)
{
	DSP4.waiting4command = FALSE;

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

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

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

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

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

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

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

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

		// 1. Viewer x-position at the next
		// 2. Viewer y-position below the horizon
		// 3. Number of raster lines drawn in this iteration
		DSP4_CLEAR_OUT();
		DSP4_WRITE_WORD(DSP4.view_x2);
		DSP4_WRITE_WORD(DSP4.view_y2);

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

		// SR = 0x00

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

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

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

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

		// SR = 0x80

		DSP4_WRITE_WORD(DSP4.segments);

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

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

			// SR = 0x00

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

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

			// SR = 0x80

			// rasterize line
			for (DSP4.lcv = 0; DSP4.lcv < DSP4.segments; DSP4.lcv++)
			{
				// 1. HDMA memory pointer (bg2)
				// 2. vertical scroll offset ($2110)
				// 3. horizontal scroll offset ($210F)
				DSP4_WRITE_WORD(DSP4.poly_ptr[0][0]);
				DSP4_WRITE_WORD((y_scroll + 0x8000) >> 16);
				DSP4_WRITE_WORD((x_scroll + 0x8000) >> 16);

				// update memory address
				DSP4.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 raster line drawn
		DSP4.view_x1    = DSP4.view_x2;
		DSP4.view_y1    = DSP4.view_y2;
		DSP4.view_xofs1 = DSP4.view_xofs2;
		DSP4.view_yofs1 = DSP4.view_yofs2;

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

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

		resume1:

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

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

		resume2:

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

	DSP4.waiting4command = TRUE;
}

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

	DSP4.waiting4command = FALSE;

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

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

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

	DSP4.poly_clipLf[0][0] = DSP4_READ_WORD();
	DSP4.poly_clipLf[0][1] = DSP4_READ_WORD();
	DSP4.poly_clipLf[1][0] = DSP4_READ_WORD();
	DSP4.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.poly_cx[0][0] = DSP4_READ_WORD();
	DSP4.poly_cx[0][1] = DSP4_READ_WORD();
	DSP4.poly_cx[1][0] = DSP4_READ_WORD();
	DSP4.poly_cx[1][1] = DSP4_READ_WORD();

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

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

	// top boundary line to clip
	DSP4.poly_top[0][0] = DSP4_READ_WORD();
	DSP4.poly_top[0][1] = DSP4_READ_WORD();
	DSP4.poly_top[1][0] = DSP4_READ_WORD();
	DSP4.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.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.poly_start[0] = view_x[0];
	DSP4.poly_start[1] = view_x[1];

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

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

	// SR = 0x00

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

	// saturate offscreen data for polygon #1
	if (win_left  < DSP4.poly_clipLf[0][0])
		win_left  = DSP4.poly_clipLf[0][0];
	if (win_left  > DSP4.poly_clipRt[0][0])
		win_left  = DSP4.poly_clipRt[0][0];
	if (win_right < DSP4.poly_clipLf[0][1])
		win_right = DSP4.poly_clipLf[0][1];
	if (win_right > DSP4.poly_clipRt[0][1])
		win_right = DSP4.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	polygon;

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

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

		resume1:

		// terminate op
		DSP4.distance = DSP4_READ_WORD();
		if (DSP4.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	left_inc, right_inc;
			int16	x1_final, x2_final;
			int16	env[2][2];
			int16	poly;

			// SR = 0x00

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

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

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

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

			// SR = 0x80

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

			// normal parameters
			poly = polygon;

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

			// scan next command if no SR check needed
			if (DSP4.segments)
			{
				int32	w_left, w_right;

				// road turnoff selection
				if ((uint16) envelope[polygon][0] == (uint16) 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.poly_plane[poly] >> 15;
				env[0][1] = envelope[polygon][0] * DSP4.distance >> 15;

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

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

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

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

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

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

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

				w_left  = SEX16(DSP4.poly_cx[polygon][0] - DSP4.poly_start[poly] + env[0][0]);
				w_right = SEX16(DSP4.poly_cx[polygon][1] - DSP4.poly_start[poly] + env[1][0]);

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

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

					// project new coordinates
					w_left  += left_inc;
					w_right += right_inc;

					// grab integer portion, drop fraction (no rounding)
					x_left  = w_left  >> 16;
					x_right = w_right >> 16;

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

					// 1. HDMA memory pointer
					// 2. Left window position ($2126/$2128)
					// 3. Right window position ($2127/$2129)
					DSP4_WRITE_WORD(DSP4.poly_ptr[polygon][0]);
					DSP4_WRITE_BYTE(x_left  & 0xff);
					DSP4_WRITE_BYTE(x_right & 0xff);

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

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

			// new projection spot to continue rasterizing from
			DSP4.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 (void)
{
	DSP4.waiting4command = FALSE;

	// op flow control
	switch (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.viewport_cx     = DSP4_READ_WORD();
	DSP4.viewport_cy     = DSP4_READ_WORD();
	DSP4_READ_WORD(); // 0x0000
	DSP4.viewport_left   = DSP4_READ_WORD();
	DSP4.viewport_right  = DSP4_READ_WORD();
	DSP4.viewport_top    = DSP4_READ_WORD();
	DSP4.viewport_bottom = DSP4_READ_WORD();

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

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

		DSP4.in_count = 4;
		DSP4_WAIT(1);

		resume1:

		////////////////////////////////////////////////
		// raster overdraw check

		DSP4.raster = DSP4_READ_WORD();

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

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

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

		// no sprite
		if (DSP4.distance == 0x0000)
			continue;

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

		// vehicle sprite
		if ((uint16) DSP4.distance == 0x9000)
		{
			int16	car_left, car_right, car_back;
			int16	impact_left, impact_back;
			int16	world_spx, world_spy;
			int16	view_spx, view_spy;
			uint16	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.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.distance >> 15;
			view_spy = world_spy * DSP4.distance >> 15;

			// convert to screen values
			DSP4.sprite_x = DSP4.viewport_cx + view_spx;
			DSP4.sprite_y = DSP4.viewport_bottom - (DSP4.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.sprite_y += DSP4_READ_WORD();
		}
		// terrain sprite
		else
		{
			int16	world_spx, world_spy;
			int16	view_spx, view_spy;

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

			resume4:

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

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

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

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

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

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

		do
		{
			int16	sp_x, sp_y, sp_attr, sp_dattr;
			int16	sp_dx, sp_dy;
			int16	pixels;
			uint16	header;
			bool8	draw;

			DSP4.in_count = 2;
			DSP4_WAIT(5);

			resume5:

			draw = TRUE;

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

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

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

			// check for valid sprite header
			header = DSP4.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.raster;
			sp_dy = DSP4_READ_WORD();
			sp_dx = DSP4_READ_WORD();

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

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

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

			DSP4_CLEAR_OUT();

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

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

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

	terminate:
	DSP4.waiting4command = TRUE;
}

static void DSP4_OP0A (int16 n2, int16 *o1, int16 *o2, int16 *o3, int16 *o4)
{
	const uint16	OP0A_Values[16] =
	{
		0x0000, 0x0030, 0x0060, 0x0090, 0x00c0, 0x00f0, 0x0120, 0x0150,
		0xfe80, 0xfeb0, 0xfee0, 0xff10, 0xff40, 0xff70, 0xffa0, 0xffd0
	};

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

static void DSP4_OP0B (bool8 *draw, int16 sp_x, int16 sp_y, int16 sp_attr, bool8 size, bool8 stop)
{
	int16	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.OAM_Row[Row1] + 1 >= DSP4.OAM_RowMax)
			*draw = 0;
		if (DSP4.OAM_Row[Row2] + 1 >= DSP4.OAM_RowMax)
			*draw = 0;
	}
	else
	{
		if (DSP4.OAM_Row[Row1] >= DSP4.OAM_RowMax)
			*draw = 0;
	}

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

	// SR = 0x80

	if (*draw)
	{
		// Row tiles
		if (size)
		{
			DSP4.OAM_Row[Row1] += 2;
			DSP4.OAM_Row[Row2] += 2;
		}
		else
			DSP4.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.sprite_count++;

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

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

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

static void DSP4_OP0D (void)
{
	DSP4.waiting4command = FALSE;

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

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

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

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

	// first raster line
	DSP4.poly_raster[0][0] = DSP4.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.view_x2    = (((DSP4.world_x + DSP4.world_xenv) >> 16) * DSP4.distance >> 15) + (DSP4.view_turnoff_x * DSP4.distance >> 15);
		DSP4.view_y2    = (DSP4.world_y >> 16) * DSP4.distance >> 15;
		DSP4.view_xofs2 = DSP4.view_x2;
		DSP4.view_yofs2 = (DSP4.world_yofs * DSP4.distance >> 15) + DSP4.poly_bottom[0][0] - DSP4.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 raster lines drawn in this iteration
		DSP4_CLEAR_OUT();
		DSP4_WRITE_WORD((DSP4.world_x + DSP4.world_xenv) >> 16);
		DSP4_WRITE_WORD(DSP4.view_x2);
		DSP4_WRITE_WORD(DSP4.world_y >> 16);
		DSP4_WRITE_WORD(DSP4.view_y2);

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

		// SR = 0x00

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

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

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

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

		// SR = 0x80

		DSP4_WRITE_WORD(DSP4.segments);

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

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

			// SR = 0x00

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

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

			// SR = 0x80

			// rasterize line
			for (DSP4.lcv = 0; DSP4.lcv < DSP4.segments; DSP4.lcv++)
			{
				// 1. HDMA memory pointer (bg1)
				// 2. vertical scroll offset ($210E)
				// 3. horizontal scroll offset ($210D)
				DSP4_WRITE_WORD(DSP4.poly_ptr[0][0]);
				DSP4_WRITE_WORD((y_scroll + 0x8000) >> 16);
				DSP4_WRITE_WORD((x_scroll + 0x8000) >> 16);

				// update memory address
				DSP4.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 raster line drawn
		DSP4.view_x1    = DSP4.view_x2;
		DSP4.view_y1    = DSP4.view_y2;
		DSP4.view_xofs1 = DSP4.view_xofs2;
		DSP4.view_yofs1 = DSP4.view_yofs2;

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

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

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

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

		resume1:

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

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

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

		resume2:

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

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

	DSP4.waiting4command = TRUE;
}

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

static void DSP4_OP0F (void)
{
	DSP4.waiting4command = FALSE;

	// op flow control
	switch (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.world_y           = DSP4_READ_DWORD();
	DSP4.poly_bottom[0][0] = DSP4_READ_WORD();
	DSP4.poly_top[0][0]    = DSP4_READ_WORD();
	DSP4.poly_cx[1][0]     = DSP4_READ_WORD();
	DSP4.viewport_bottom   = DSP4_READ_WORD();
	DSP4.world_x           = DSP4_READ_DWORD();
	DSP4.poly_cx[0][0]     = DSP4_READ_WORD();
	DSP4.poly_ptr[0][0]    = DSP4_READ_WORD();
	DSP4.world_yofs        = DSP4_READ_WORD();
	DSP4.world_dy          = DSP4_READ_DWORD();
	DSP4.world_dx          = DSP4_READ_DWORD();
	DSP4.distance          = DSP4_READ_WORD();
	DSP4_READ_WORD(); // 0x0000
	DSP4.world_xenv        = DSP4_READ_DWORD();
	DSP4.world_ddy         = DSP4_READ_WORD();
	DSP4.world_ddx         = DSP4_READ_WORD();
	DSP4.view_yofsenv      = DSP4_READ_WORD();

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

	// first raster line
	DSP4.poly_raster[0][0] = DSP4.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.view_x2    = ((DSP4.world_x + DSP4.world_xenv) >> 16) * DSP4.distance >> 15;
		DSP4.view_y2    = (DSP4.world_y >> 16) * DSP4.distance >> 15;
		DSP4.view_xofs2 = DSP4.view_x2;
		DSP4.view_yofs2 = (DSP4.world_yofs * DSP4.distance >> 15) + DSP4.poly_bottom[0][0] - DSP4.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 raster lines drawn in this iteration
		DSP4_CLEAR_OUT();
		DSP4_WRITE_WORD((DSP4.world_x + DSP4.world_xenv) >> 16);
		DSP4_WRITE_WORD(DSP4.view_x2);
		DSP4_WRITE_WORD(DSP4.world_y >> 16);
		DSP4_WRITE_WORD(DSP4.view_y2);

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

		// SR = 0x00

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

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

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

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

		// SR = 0x80

		DSP4_WRITE_WORD(DSP4.segments);

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

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

			for (DSP4.lcv = 0; DSP4.lcv < 4; DSP4.lcv++)
			{
				// grab inputs
				DSP4.in_count = 4;
				DSP4_WAIT(1);

				resume1:

				for (;;)
				{
					int16	dist;
					int16	color, red, green, blue;

					dist  = 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   * dist >> 15) & 0x1f;
					green = (green * dist >> 15) & 0x1f;
					blue  = (blue  * dist >> 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.view_xofs2 - DSP4.view_xofs1) * DSP4_Inverse(DSP4.segments) << 1;
			py_dy = (DSP4.view_yofs2 - DSP4.view_yofs1) * DSP4_Inverse(DSP4.segments) << 1;

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

			// SR = 0x80

			// rasterize line
			for (DSP4.lcv = 0; DSP4.lcv < DSP4.segments; DSP4.lcv++)
			{
				// 1. HDMA memory pointer
				// 2. vertical scroll offset ($210E)
				// 3. horizontal scroll offset ($210D)
				DSP4_WRITE_WORD(DSP4.poly_ptr[0][0]);
				DSP4_WRITE_WORD((y_scroll + 0x8000) >> 16);
				DSP4_WRITE_WORD((x_scroll + 0x8000) >> 16);

				// update memory address
				DSP4.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 raster line drawn
		DSP4.view_x1    = DSP4.view_x2;
		DSP4.view_y1    = DSP4.view_y2;
		DSP4.view_xofs1 = DSP4.view_xofs2;
		DSP4.view_yofs1 = DSP4.view_yofs2;

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

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

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

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

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

		resume2:

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

		// road splice
		if ((uint16) DSP4.distance == 0x8001)
		{
			DSP4.in_count = 6;
			DSP4_WAIT(3);

			resume3:

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

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

			// update stepping values
			DSP4.view_turnoff_x += DSP4.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.world_ddy    = DSP4_READ_WORD();
		DSP4.world_ddx    = DSP4_READ_WORD();
		DSP4.view_yofsenv = DSP4_READ_WORD();

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

	// terminate op
	DSP4.waiting4command = TRUE;
}

static void DSP4_OP10 (void)
{
	DSP4.waiting4command = FALSE;

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

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

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

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

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

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

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

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

		// 1. Viewer x-position at the next
		// 2. Viewer y-position below the horizon
		// 3. Number of raster lines drawn in this iteration
		DSP4_CLEAR_OUT();
		DSP4_WRITE_WORD(DSP4.view_x2);
		DSP4_WRITE_WORD(DSP4.view_y2);

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

		// SR = 0x00

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

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

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

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

		// SR = 0x80

		DSP4_WRITE_WORD(DSP4.segments);

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

		// scan next command if no SR check needed
		if (DSP4.segments)
		{
			for (DSP4.lcv = 0; DSP4.lcv < 4; DSP4.lcv++)
			{
				// grab inputs
				DSP4.in_count = 4;
				DSP4_WAIT(1);

				resume1:

				for (;;)
				{
					int16	dist;
					int16	color, red, green, blue;

					dist  = 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   * dist >> 15) & 0x1f;
					green = (green * dist >> 15) & 0x1f;
					blue  = (blue  * dist >> 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.segments)
		{
			int32	px_dx, py_dy;
			int32	x_scroll, y_scroll;

			// SR = 0x00

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

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

			// SR = 0x80

			// rasterize line
			for (DSP4.lcv = 0; DSP4.lcv < DSP4.segments; DSP4.lcv++)
			{
				// 1. HDMA memory pointer (bg2)
				// 2. vertical scroll offset ($2110)
				// 3. horizontal scroll offset ($210F)
				DSP4_WRITE_WORD(DSP4.poly_ptr[0][0]);
				DSP4_WRITE_WORD((y_scroll + 0x8000) >> 16);
				DSP4_WRITE_WORD((x_scroll + 0x8000) >> 16);

				// update memory address
				DSP4.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 raster line drawn
		DSP4.view_x1    = DSP4.view_x2;
		DSP4.view_y1    = DSP4.view_y2;
		DSP4.view_xofs1 = DSP4.view_xofs2;
		DSP4.view_yofs1 = DSP4.view_yofs2;

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

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

		resume2:

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

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

		resume3:

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

	DSP4.waiting4command = TRUE;
}

static void DSP4_OP11 (int16 A, int16 B, int16 C, int16 D, int16 *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);
}

static void DSP4_SetByte (void)
{
	// 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.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	multiplier, multiplicand;
				int32	product;

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

				DSP4_Multiply(multiplicand, multiplier, &product);

				DSP4_CLEAR_OUT();
				DSP4_WRITE_WORD(product);
				DSP4_WRITE_WORD(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:
			{
				DSP4_READ_WORD();
				int16	in2a = DSP4_READ_WORD();
				DSP4_READ_WORD();
				int16	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	sp_x    = DSP4_READ_WORD();
				int16	sp_y    = DSP4_READ_WORD();
				int16	sp_attr = DSP4_READ_WORD();
				bool8	draw = TRUE;

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

static void DSP4_GetByte (void)
{
	if (DSP4.out_count)
	{
		DSP4.byte = (uint8) DSP4.output[DSP4.out_index & 0x1FF];

		DSP4.out_index++;
		if (DSP4.out_count == DSP4.out_index)
			DSP4.out_count = 0;
	}
	else
		DSP4.byte = 0xff;
}

void DSP4SetByte (uint8 byte, uint16 address)
{
	if (address < DSP0.boundary)
	{
		DSP4.byte    = byte;
		DSP4.address = address;
		DSP4_SetByte();
	}
}

uint8 DSP4GetByte (uint16 address)
{
	if (address < DSP0.boundary)
	{
		DSP4.address = address;
		DSP4_GetByte();
		return (DSP4.byte);
	}

	return (0x80);
}
