view jag_video.c @ 2069:8e51c0c3f2e3 segacd

Initial attempt at implementing the Sega CD graphics hardware
author Michael Pavone <pavone@retrodev.com>
date Sun, 30 Jan 2022 19:55:33 -0800
parents 653558f6fa7a
children
line wrap: on
line source

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include "jag_video.h"
#include "jaguar.h"
#include "render.h"

enum {
	VMODE_CRY,
	VMODE_RGB24,
	VMODE_DIRECT16,
	VMODE_RGB16,
	VMODE_VARIABLE
};

#define BIT_TBGEN 1

char *vmode_names[] = {
	"CRY",
	"RGB24",
	"RGB16",
	"DIRECT16",
	"VARIABLE"
};

static uint8_t cry_red[9][16] = {
	{0, 34, 68, 102, 135, 169, 203, 237, 255, 255, 255, 255, 255, 255, 255, 255},
	{0, 34, 68, 102, 135, 169, 203, 230, 247, 255, 255, 255, 255, 255, 255, 255},
	{0, 34, 68, 102, 135, 170, 183, 197, 214, 235, 255, 255, 255, 255, 255, 255},
	{0, 34, 68, 102, 130, 141, 153, 164, 181, 204, 227, 249, 255, 255, 255, 255},
	{0, 34, 68, 95,  104, 113, 122, 131, 148, 173, 198, 223, 248, 255, 255, 255},
	{0, 34, 64, 71,  78,  85,  91,  98,  115, 143, 170, 197, 224, 252, 255, 255},
	{0, 34, 43, 47,  52,  56,  61,  65,  82,  112, 141, 171, 200, 230, 255, 255},
	{0, 19, 21, 23,  26,  28,  30,  32,  49,  81,  113, 145, 177, 208, 240, 255},
	{0, 0,  0,  0,   0,   0,   0,   0,   17,  51,  85,  119, 153, 187, 221, 255}
};

static uint8_t cry_green[16][8] = {
	{0,   0,   0,   0,   0,   0,   0,   0},
	{17,  19,  21,  23,  26,  28,  30,  32},
	{34,  38,  43,  47,  52,  56,  61,  65},
	{51,  57,  64,  71,  78,  85,  91,  98},
	{68,  77,  86,  95,  104, 113, 122, 131},
	{85,  96,  107, 119, 130, 141, 153, 164},
	{102, 115, 129, 142, 156, 170, 183, 197},
	{119, 134, 150, 166, 182, 198, 214, 230},
	{136, 154, 172, 190, 208, 226, 244, 255},
	{153, 173, 193, 214, 234, 255, 255, 255},
	{170, 192, 215, 238, 255, 255, 255, 255},
	{187, 211, 236, 255, 255, 255, 255, 255},
	{204, 231, 255, 255, 255, 255, 255, 255},
	{221, 250, 255, 255, 255, 255, 255, 255},
	{238, 255, 255, 255, 255, 255, 255, 255},
	{255, 255, 255, 255, 255, 255, 255, 255},
};

static uint32_t table_cry[0x10000];
static uint32_t table_rgb[0x10000];
static uint32_t table_variable[0x10000];

static uint32_t cry_to_rgb(uint16_t cry)
{
	uint32_t y = cry & 0xFF;
	if (y) {
		uint8_t c = cry >> 8 & 0xF;
		uint8_t r = cry >> 12;
		
		uint32_t red = cry_red[c < 7 ? 0 : c - 7][r];
		uint32_t green = cry_green[c][r < 8 ? r : 15 - r];
		uint32_t blue = cry_red[c < 7 ? 0 : c - 7][15-r];
		red = red * y / 255;
		blue = blue * y / 255;
		green = green * y / 255;
		return render_map_color(red, green, blue);
	} else {
		return render_map_color(0, 0, 0);
	}
}

static uint32_t rgb16_to_rgb(uint16_t rgb)
{
	return render_map_color(
		rgb >> 8 & 0xF8,
		rgb << 2 & 0xFC,
		rgb >> 4 & 0xF8
	);
}

jag_video *jag_video_init(void)
{
	static uint8_t table_init_done = 0;
	if (!table_init_done) {
		for (int i = 0; i < 0x10000; i++)
		{
			table_cry[i] = cry_to_rgb(i);
			table_rgb[i] = rgb16_to_rgb(i);
			table_variable[i] = i & 1 ? rgb16_to_rgb(i & 0xFFFE) : cry_to_rgb(i);
		}
		table_init_done = 1;
	}
	return calloc(1, sizeof(jag_video));
}

static void copy_16(uint32_t *dst, uint32_t len, uint16_t *linebuffer, uint32_t *table)
{
	for (; len; len--, dst++, linebuffer++)
	{
		*dst = table[*linebuffer];
	}
}

static void copy_linebuffer(jag_video *context, uint16_t *linebuffer)
{
	if (!context->output) {
		return;
	}
	uint32_t *dst = context->output;
	uint32_t len;
	if (context->regs[VID_HCOUNT] == context->regs[VID_HDISP_BEGIN1]) {
		if (
			context->regs[VID_HDISP_BEGIN2] == context->regs[VID_HDISP_BEGIN1] 
			|| context->regs[VID_HDISP_BEGIN2] > (context->regs[VID_HPERIOD] | 0x400)
		) {
			//only one line buffer per line, so copy the previous line in its entirety
			len = context->regs[VID_HDISP_END] - 0x400 + context->regs[VID_HPERIOD] - context->regs[VID_HDISP_BEGIN1] + 2;
		} else {
			//copy the second half of the previous line
			if (context->regs[VID_HDISP_BEGIN2] & 0x400) {
				//BEGIN2 is after the HCOUNT jump
				dst += context->regs[VID_HPERIOD] - context->regs[VID_HDISP_BEGIN1] 
					+ context->regs[VID_HDISP_BEGIN2] - 0x400 + 1;
				len = context->regs[VID_HDISP_END] - context->regs[VID_HDISP_BEGIN2] + 1;
			} else {
				//BEGIN2 is before the HCOUNT jump
				dst += context->regs[VID_HDISP_BEGIN2] - context->regs[VID_HDISP_BEGIN1];
				len = context->regs[VID_HDISP_END] + context->regs[VID_HPERIOD] - context->regs[VID_HDISP_BEGIN2] + 2;
			}
		}
		context->output += context->output_pitch / sizeof(uint32_t);
	} else {
		//copy the first half of the current line
		if (context->regs[VID_HDISP_BEGIN2] & 0x400) {
			//BEGIN2 is after the HCOUNT jump
			len = context->regs[VID_HDISP_BEGIN2] - 0x400 + context->regs[VID_HPERIOD] - context->regs[VID_HDISP_BEGIN1] + 1;
		} else {
			//BEGIN2 is before the HCOUNT jump
			len = context->regs[VID_HDISP_BEGIN2] - context->regs[VID_HDISP_BEGIN1];
		}
	}
	len /= context->pclock_div;
	switch (context->mode)
	{
	case VMODE_CRY:
		copy_16(dst, len, linebuffer, table_cry);
		break;
	case VMODE_RGB24:
		//TODO: Implement me
		break;
	case VMODE_DIRECT16:
		//TODO: Implement this once I better understand what would happen on hardware with composite output
		break;
	case VMODE_RGB16:
		copy_16(dst, len, linebuffer, table_rgb);
		break;
	case VMODE_VARIABLE:
		copy_16(dst, len, linebuffer, table_variable);
		break;
	}
}

enum {
	OBJ_IDLE,
	OBJ_FETCH_DESC1,
	OBJ_FETCH_DESC2,
	OBJ_FETCH_DESC3,
	OBJ_PROCESS,
	OBJ_HEIGHT_WB,
	OBJ_REMAINDER_WB,
	OBJ_GPU_WAIT
};

enum {
	OBJ_BITMAP,
	OBJ_SCALED,
	OBJ_GPU,
	OBJ_BRANCH,
	OBJ_STOP
};

uint32_t jag_cycles_to_halfline(jag_video *context, uint32_t target)
{
	uint32_t cycles = context->regs[VID_HPERIOD] - (context->regs[VID_HCOUNT] & 0x3FF);
	uint32_t num_lines;
	if (context->regs[VID_VCOUNT] < target) {
		num_lines = target - 1 - context->regs[VID_VCOUNT];
	} else {
		num_lines = target + context->regs[VID_VPERIOD] - context->regs[VID_VCOUNT];
	}
	cycles += num_lines * context->regs[VID_HPERIOD];
	return cycles;
}

uint32_t jag_next_vid_interrupt(jag_video *context)
{
	if (context->regs[VID_VINT] > context->regs[VID_VPERIOD]) {
		return 0xFFFFFFF;
	}
	return context->cycles + jag_cycles_to_halfline(context, context->regs[VID_VINT]);
}

void op_run(jag_video *context)
{
	while (context->op.cycles < context->cycles)
	{
		switch (context->op.state)
		{
		case OBJ_IDLE:
		case OBJ_GPU_WAIT:
			context->op.cycles = context->cycles;
			break;
		case OBJ_FETCH_DESC1: {
			uint32_t address = context->regs[VID_OBJLIST1] | context->regs[VID_OBJLIST2] << 16;
			uint64_t val = jag_read_phrase(context->system, address, &context->op.cycles);
			address += 8;
				
			context->regs[VID_OBJ0] = val >> 48;
			context->regs[VID_OBJ1] = val >> 32;
			context->regs[VID_OBJ2] = val >> 16;
			context->regs[VID_OBJ3] = val;
			context->op.type = val & 7;
			context->op.has_prefetch = 0;
			uint16_t ypos = val >> 3 & 0x7FF;
			switch (context->op.type)
			{
			case OBJ_BITMAP:
			case OBJ_SCALED: {
				uint16_t height = val >> 14 & 0x7FF;
				uint32_t link = (address & 0xC00007) | (val >> 21 & 0x3FFFF8);
				if ((ypos == 0x7FF || context->regs[VID_VCOUNT] >= ypos) && height) {
					context->op.state = OBJ_FETCH_DESC2;
					context->op.obj_start = address - 8;
					context->op.ypos = ypos;
					context->op.height = height;
					context->op.link = link;
					context->op.data_address = val >> 40 & 0xFFFFF8;
					context->op.cur_address = context->op.data_address;
				} else {
					//object is not visible on this line, advance to next object
					address = link;
				}
				break;
			}
			case OBJ_GPU:
				context->op.state = OBJ_GPU_WAIT;
				break;
			case OBJ_BRANCH: {
				uint8_t branch;
				switch(val >> 14 & 7)
				{
				case 0:
					branch = ypos == context->regs[VID_VCOUNT] || ypos == 0x7FF;
					break;
				case 1:
					branch = ypos > context->regs[VID_VCOUNT];
					break;
				case 2:
					branch = ypos < context->regs[VID_VCOUNT];
					break;
				case 3:
					branch = context->regs[VID_OBJFLAG] & 1;
					break;
				case 4:
					branch = (context->regs[VID_HCOUNT] & 0x400) != 0;
					break;
				default:
					branch = 0;
					fprintf(stderr, "Invalid branch CC type %d in object at %X\n", (int)(val >> 14 & 7), address-8);
					break;
				}
				if (branch) {
					address &= 0xC00007;
					address |= val >> 21 & 0x3FFFF8;
				}
			}
			case OBJ_STOP:
				//TODO: trigger interrupt
				context->op.state = OBJ_IDLE;
				break;
			}
			context->regs[VID_OBJLIST1] = address;
			context->regs[VID_OBJLIST2] = address >> 16;
			break;
		}
		case OBJ_FETCH_DESC2: {
			uint32_t address = context->regs[VID_OBJLIST1] | context->regs[VID_OBJLIST2] << 16;
			uint64_t val = jag_read_phrase(context->system, address, &context->op.cycles);
			address += 8;
			
			context->op.xpos = val & 0xFFF;
			if (context->op.xpos & 0x800) {
				context->op.xpos |= 0xF000;
			}
			context->op.increment = (val >> 15 & 0x7) * 8;
			context->op.bpp = 1 << (val >> 12 & 7);
			if (context->op.bpp == 32) {
				context->op.bpp = 24;
			}
			context->op.line_pitch = (val >> 18 & 0x3FF) * 8;
			if (context->op.bpp < 8) {
				context->op.pal_offset = val >> 37;
				if (context->op.bpp == 4) {
					context->op.pal_offset &= 0xF0;
				} else if(context->op.bpp == 2) {
					context->op.pal_offset &= 0xFC;
				} else {
					context->op.pal_offset &= 0xFE;
				}
			} else {
				context->op.pal_offset = 0;
			}
			context->op.line_phrases = val >> 28 & 0x3FF;
			context->op.hflip = (val & (1UL << 45)) != 0;
			context->op.addpixels = (val & (1UL << 46)) != 0;
			context->op.transparent = (val & (1UL << 47)) != 0;
			//TODO: do something with RELEASE flag
			context->op.leftclip = val >> 49;
			if (context->op.type == OBJ_SCALED) {
				context->op.state = OBJ_FETCH_DESC3;
				switch (context->op.bpp)
				{
				case 1:
					context->op.leftclip &= 0x3F;
					
					break;
				//documentation isn't clear exactly how this works for higher bpp values
				case 2:
					context->op.leftclip &= 0x3E;
					break;
				case 4:
					context->op.leftclip &= 0x3C;
					break;
				case 8:
					context->op.leftclip &= 0x38;
					break;
				case 16:
					context->op.leftclip &= 0x30;
					break;
				default:
					context->op.leftclip = 0x20;
					break;
				}
			} else {
				context->op.state = OBJ_PROCESS;
				address = context->op.link;
				switch (context->op.bpp)
				{
				case 1:
					context->op.leftclip &= 0x3E;
					break;
				case 2:
					context->op.leftclip &= 0x3C;
					break;
				//values for 4bpp and up are sort of a guess
				case 4:
					context->op.leftclip &= 0x38;
					break;
				case 8:
					context->op.leftclip &= 0x30;
					break;
				case 16:
					context->op.leftclip &= 0x20;
					break;
				default:
					context->op.leftclip = 0;
					break;
				}
			}
			if (context->op.xpos < 0) {
				int16_t pixels_per_phrase = 64 / context->op.bpp;
				int16_t clip = -context->op.xpos / pixels_per_phrase;
				int16_t rem = -context->op.xpos % pixels_per_phrase;
				if (clip >= context->op.line_phrases) {
					context->op.line_phrases = 0;
				} else {
					context->op.line_phrases -= clip;
					context->op.leftclip += rem * context->op.bpp;
					if (context->op.leftclip >= 64) {
						context->op.line_phrases--;
						context->op.leftclip -= 64;
					}
					
				}
			} else if (context->op.bpp < 32){
				context->op.lb_offset = context->op.xpos;
			} else {
				context->op.lb_offset = context->op.xpos * 2;
			}
			if (context->op.lb_offset >= LINEBUFFER_WORDS || !context->op.line_phrases) {
				//ignore objects that are completely offscreen
				//not sure if that's how the hardware does it, but it would make sense
				context->op.state = OBJ_FETCH_DESC1;
				address = context->op.link;
			}
			context->regs[VID_OBJLIST1] = address;
			context->regs[VID_OBJLIST2] = address >> 16;
			break;
		}
		case OBJ_FETCH_DESC3: {
			uint32_t address = context->regs[VID_OBJLIST1] | context->regs[VID_OBJLIST2] << 16;
			uint64_t val = jag_read_phrase(context->system, address, &context->op.cycles);
			
			context->op.state = OBJ_PROCESS;
			context->op.hscale = val & 0xFF;;
			context->op.hremainder = val & 0xFF;
			context->op.vscale = val >> 8 & 0xFF;
			context->op.remainder = val >> 16 & 0xFF;
			
			context->regs[VID_OBJLIST1] = context->op.link;
			context->regs[VID_OBJLIST2] = context->op.link >> 16;
			break;
		}
		case OBJ_PROCESS: {
			uint32_t proc_cycles = 0;
			if (!context->op.has_prefetch && context->op.line_phrases) {
				context->op.prefetch = jag_read_phrase(context->system, context->op.cur_address, &proc_cycles);
				context->op.cur_address += context->op.increment;
				context->op.has_prefetch = 1;
				context->op.line_phrases--;
			}
			if (!proc_cycles) {
				//run at least one cycle of writes even if we didn't spend any time reading
				proc_cycles = 1;
			}
			while (proc_cycles)
			{
				if (context->op.im_bits) {
					uint32_t val = context->op.im_data >> (context->op.im_bits - context->op.bpp);
					val &= (1 << context->op.bpp) - 1;
					if (val || !context->op.transparent)
					{
						if (context->op.bpp < 16) {
							val = context->clut[val + context->op.pal_offset];
						}
						if (context->op.bpp == 32) {
							context->write_line_buffer[context->op.lb_offset++] = val >> 16;
						}
						context->write_line_buffer[context->op.lb_offset++] = val;
					} else {
						context->op.lb_offset += context->op.bpp == 32 ? 2 : 1;
					}
					if (context->op.type == OBJ_SCALED) {
						context->op.hremainder -= 0x20;
						while (context->op.hremainder <= 0 && context->op.im_bits) {
							context->op.im_bits -= context->op.bpp;
							context->op.hremainder += context->op.hscale;
						}
					} else {
						context->op.im_bits -= context->op.bpp;
					}
				}
				if (context->op.im_bits && context->op.bpp < 32 && context->op.type == OBJ_BITMAP && context->op.lb_offset < LINEBUFFER_WORDS) {
					uint32_t val = context->op.im_data >> (context->op.im_bits - context->op.bpp);
					val &= (1 << context->op.bpp) - 1;
					if (val || !context->op.transparent)
					{
						val = context->clut[val + context->op.pal_offset];
						context->write_line_buffer[context->op.lb_offset] = val;
					}
					context->op.lb_offset++;
					context->op.im_bits -= context->op.bpp;
				}
				context->op_cycles++;
				proc_cycles--;
			}
			if (!context->op.im_bits && context->op.has_prefetch) {
				context->op.im_data = context->op.prefetch;
				context->op.has_prefetch = 0;
				//docs say this is supposed to be a value in pixels
				//but given the "significant" bits part I'm guessing
				//this is actually how many bits are pre-shifted off
				//the first phrase read in a line
				context->op.im_bits = 64 - context->op.leftclip;
				context->op.leftclip = 0;
			}
			if (context->op.lb_offset == LINEBUFFER_WORDS || (!context->op.im_bits && !context->op.line_phrases)) {
				context->op.state = OBJ_HEIGHT_WB;
			}
			break;
		}
		case OBJ_HEIGHT_WB: {
			if (context->op.type == OBJ_BITMAP) {
				context->op.height--;
				context->op.data_address += context->op.line_pitch;
				context->op.state = OBJ_FETCH_DESC1;
			} else {
				context->op.remainder -= 0x20;
				context->op.state = OBJ_REMAINDER_WB;
				while (context->op.height && context->op.remainder <= 0) {
					context->op.height--;
					context->op.remainder += context->op.vscale;
					context->op.data_address += context->op.line_pitch;
				}
			}
			uint64_t val = context->op.type | context->op.ypos << 3  | context->op.height << 14
				| ((uint64_t)context->op.link & 0x3FFFF8) << 21 | ((uint64_t)context->op.data_address) << 40;
			context->op.cycles += jag_write_phrase(context->system, context->op.obj_start, val);
			break;
		}
		case OBJ_REMAINDER_WB: {
			uint64_t val = context->op.hscale | context->op.vscale << 8 | context->op.remainder << 16;
			context->op.cycles += jag_write_phrase(context->system, context->op.obj_start+16, val);
			context->op.state = OBJ_FETCH_DESC1;
			break;
		}
		}
	}
}

void jag_video_run(jag_video *context, uint32_t target_cycle)
{
	if (context->regs[VID_VMODE] & BIT_TBGEN) {
		while (context->cycles < target_cycle)
		{
			//TODO: Optimize this to not actually increment one step at a time
			if (
				(
					context->regs[VID_HCOUNT] == context->regs[VID_HDISP_BEGIN1]
					|| context->regs[VID_HCOUNT] == context->regs[VID_HDISP_BEGIN2]
				)
				&& context->regs[VID_VCOUNT] >= context->regs[VID_VDISP_BEGIN]
				&& context->regs[VID_VCOUNT] < context->regs[VID_VDISP_END]
			) {
				//swap linebuffers, render linebuffer to framebuffer and kick off object processor
				if (context->write_line_buffer == context->line_buffer_a) {
					context->write_line_buffer = context->line_buffer_b;
					copy_linebuffer(context, context->line_buffer_a);
				} else {
					context->write_line_buffer = context->line_buffer_a;
					copy_linebuffer(context, context->line_buffer_b);
				}
				//clear new write line buffer with background color
				for (int i = 0; i  < LINEBUFFER_WORDS; i++)
				{
					context->write_line_buffer[i] = context->regs[VID_BGCOLOR];
				}
				
				//kick off object processor
				context->op.state = OBJ_FETCH_DESC1;
			} else if (context->regs[VID_HCOUNT] == context->regs[VID_HDISP_END]) {
				//stob object processor
				context->op.state = OBJ_IDLE;
			}
			
			context->cycles++;
			op_run(context);
			
			//advance counters
			if (
				!context->output 
				&& context->regs[VID_VCOUNT] == context->regs[VID_VDISP_BEGIN]
				&& context->regs[VID_HCOUNT] == context->regs[VID_HDISP_BEGIN1]
			) {
				context->output = render_get_framebuffer(FRAMEBUFFER_ODD, &context->output_pitch);
			} else if (context->output && context->regs[VID_VCOUNT] >= context->regs[VID_VDISP_END]) {
				int width = (context->regs[VID_HPERIOD] - context->regs[VID_HDISP_BEGIN1] 
				+ context->regs[VID_HDISP_END] - 1024 + 2) / context->pclock_div;
				render_framebuffer_updated(FRAMEBUFFER_ODD, width);
				context->output = NULL;
			}
			
			if ((context->regs[VID_HCOUNT] & 0x3FF) == context->regs[VID_HPERIOD]) {
				//reset bottom 10 bits to zero, flip the 11th bit which represents which half of the line we're on
				context->regs[VID_HCOUNT] = (context->regs[VID_HCOUNT] & 0x400) ^ 0x400;
				//increment half-line counter
				if (context->regs[VID_VCOUNT] == context->regs[VID_VPERIOD]) {
					context->regs[VID_VCOUNT] = 0;
				} else {
					context->regs[VID_VCOUNT]++;
					if (context->regs[VID_VCOUNT] == context->regs[VID_VINT]) {
						context->cpu_int_pending |= BIT_CPU_VID_INT_ENABLED;
					}
				}
			} else {
				context->regs[VID_HCOUNT]++;
			}
			
		}
	} else {
		context->cycles = target_cycle;
	}
}

static uint8_t is_reg_writeable(uint32_t address)
{
	return address < VID_HLPEN || address >= VID_OBJLIST1;
}

void jag_video_reg_write(jag_video *context, uint32_t address, uint16_t value)
{
	uint32_t reg = (address >> 1 & 0x7F) - 2;
	if (reg < JAG_VIDEO_REGS && is_reg_writeable(reg)) {
		context->regs[reg] = value;
		if (reg == VID_VMODE) {
			context->pclock_div = (value >> 9 & 7) + 1;
			context->pclock_counter = 0;
			if (value & 0x10) {
				context->mode = VMODE_VARIABLE;
			} else {
				context->mode = value >> 1 & 3;
			}
			printf("Mode %s, pixel clock divider: %d, time base generation: %s\n", vmode_names[context->mode], context->pclock_div, value & BIT_TBGEN ? "enabled" : "disabled");
		}
		switch (reg)
		{
		case VID_OBJLIST1:
			printf("Object List Pointer 1: %X\n", value);
			break;
		case VID_OBJLIST2:
			printf("Object List Pointer 2: %X\n", value);
			break;
		case VID_HPERIOD:
			printf("Horizontal period: %d\n", value & 0x3FF);
			break;
		case VID_HBLANK_BEGIN:
			printf("horizontal blanking begin: %d\n", value & 0x7FF);
			break;
		case VID_HBLANK_END:
			printf("horizontal blanking end: %d\n", value & 0x7FF);
			break;
		case VID_HSYNC:
			printf("horizontal sync start: %d\n", value & 0x7FF);
			break;
		case VID_HDISP_BEGIN1:
			printf("horizontal display begin 1: %d\n", value & 0x7FF);
			break;
		case VID_HDISP_BEGIN2:
			printf("horizontal display begin 2: %d\n", value & 0x7FF);
			break;
		case VID_HDISP_END:
			printf("horizontal display end: %d\n", value & 0x7FF);
			break;
		case VID_VPERIOD:
			printf("Vertical period: %d\n", value & 0x7FF);
			break;
		case VID_VBLANK_BEGIN:
			printf("vertical blanking begin: %d\n", value & 0x7FF);
			break;
		case VID_VBLANK_END:
			printf("vertical blanking end: %d\n", value & 0x7FF);
			break;
		case VID_VSYNC:
			printf("vertical sync start: %d\n", value & 0x7FF);
			break;
		case VID_VDISP_BEGIN:
			printf("vertical display begin: %d\n", value & 0x7FF);
			break;
		case VID_VDISP_END:
			printf("vertical display end: %d\n", value & 0x7FF);
			break;
		}
	} else {
		fprintf(stderr, "Write to invalid video/object processor register %X:%X\n", address, value);
	}
}