changeset 2243:0d1d5dccdd28

Initial implementation of oscilloscope debug view
author Michael Pavone <pavone@retrodev.com>
date Tue, 22 Nov 2022 17:57:02 -0800
parents 8e8db9141209
children e6bad7bd8751
files Makefile bindings.c config.c default.cfg genesis.c psg.c psg.h rf5c164.c rf5c164.h sms.c system.h vdp.c vdp.h ym2612.c ym2612.h
diffstat 15 files changed, 262 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Wed Sep 21 23:16:39 2022 -0700
+++ b/Makefile	Tue Nov 22 17:57:02 2022 -0800
@@ -196,7 +196,7 @@
 endif
 endif
 endif
-AUDIOOBJS=ym2612.o psg.o wave.o vgm.o event_log.o render_audio.o rf5c164.o
+AUDIOOBJS=ym2612.o psg.o wave.o vgm.o event_log.o render_audio.o rf5c164.o oscilloscope.o
 CONFIGOBJS=config.o tern.o util.o paths.o
 NUKLEAROBJS=$(FONT) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o
 RENDEROBJS=ppm.o controller_info.o
--- a/bindings.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/bindings.c	Tue Nov 22 17:57:02 2022 -0800
@@ -42,7 +42,8 @@
 	UI_PLANE_DEBUG,
 	UI_VRAM_DEBUG,
 	UI_CRAM_DEBUG,
-	UI_COMPOSITE_DEBUG
+	UI_COMPOSITE_DEBUG,
+	UI_OSCILLOSCOPE_DEBUG,
 } ui_action;
 
 typedef struct {
@@ -432,7 +433,10 @@
 		case UI_VRAM_DEBUG:
 		case UI_CRAM_DEBUG:
 		case UI_COMPOSITE_DEBUG:
-			if (allow_content_binds) {
+		case UI_OSCILLOSCOPE_DEBUG:
+			if (allow_content_binds && current_system->toggle_debug_view) {
+				current_system->toggle_debug_view(current_system, binding->subtype_a - UI_PLANE_DEBUG + DEBUG_PLANE);
+				/*
 				vdp_context *vdp = NULL;
 				if (current_system->type == SYSTEM_GENESIS || current_system->type == SYSTEM_SEGACD) {
 					genesis_context *gen = (genesis_context *)current_system;
@@ -452,7 +456,7 @@
 					default: return;
 					}
 					vdp_toggle_debug_view(vdp, debug_type);
-				}
+				}*/
 				break;
 			}
 		}
@@ -671,6 +675,8 @@
 			*subtype_a = UI_CRAM_DEBUG;
 		} else if (!strcmp(target + 3, "compositing_debug")) {
 			*subtype_a = UI_COMPOSITE_DEBUG;
+		} else if (!strcmp(target + 3, "oscilloscope")) {
+			*subtype_a = UI_OSCILLOSCOPE_DEBUG;
 		} else {
 			warning("Unreconized UI binding type %s\n", target);
 			return 0;
--- a/config.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/config.c	Tue Nov 22 17:57:02 2022 -0800
@@ -285,7 +285,7 @@
 	*pads = tern_insert_node(*pads, key, dupe_tree(val.ptrval));
 }
 
-#define CONFIG_VERSION 5
+#define CONFIG_VERSION 6
 static tern_node *migrate_config(tern_node *config, int from_version)
 {
 	tern_node *def_config = parse_bundled_config("default.cfg");
@@ -361,6 +361,10 @@
 		config = tern_insert_path(config, "io\0ea_multitap\0""3\0", (tern_val){.ptrval = strdup(tap13)}, TVAL_PTR);
 		config = tern_insert_path(config, "io\0ea_multitap\0""4\0", (tern_val){.ptrval = strdup(tap14)}, TVAL_PTR);
 	}
+	case 5: {
+		char *binding_o = tern_find_path_default(config, "bindings\0keys\0o\0", (tern_val){.ptrval = "ui.oscilloscope"}, TVAL_PTR).ptrval;
+		config = tern_insert_path(config, "bindings\0keys\0o\0", (tern_val){.ptrval = strdup(binding_o)}, TVAL_PTR);
+	}
 	}
 	char buffer[16];
 	sprintf(buffer, "%d", CONFIG_VERSION);
--- a/default.cfg	Wed Sep 21 23:16:39 2022 -0700
+++ b/default.cfg	Tue Nov 22 17:57:02 2022 -0800
@@ -21,6 +21,7 @@
 		v ui.vram_debug
 		c ui.cram_debug
 		n ui.compositing_debug
+		o ui.oscilloscope
 		m ui.vgm_log
 		esc ui.exit
 		` ui.save_state
--- a/genesis.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/genesis.c	Tue Nov 22 17:57:02 2022 -0800
@@ -467,6 +467,9 @@
 		gen->reset_cycle = CYCLE_NEVER;
 	}
 	if (v_context->frame != gen->last_frame) {
+		if (gen->ym->scope) {
+			scope_render(gen->ym->scope);
+		}
 		//printf("reached frame end %d | MCLK Cycles: %d, Target: %d, VDP cycles: %d, vcounter: %d, hslot: %d\n", gen->last_frame, mclks, gen->frame_end, v_context->cycles, v_context->vcounter, v_context->hslot);
 		uint32_t elapsed = v_context->frame - gen->last_frame;
 		gen->last_frame = v_context->frame;
@@ -1712,6 +1715,33 @@
 	gen->header.vgm_logging = 0;
 }
 
+static void toggle_debug_view(system_header *system, uint8_t debug_view)
+{
+	genesis_context *gen = (genesis_context *)system;
+	if (debug_view < DEBUG_OSCILLOSCOPE) {
+		vdp_toggle_debug_view(gen->vdp, debug_view);
+	} else if (debug_view == DEBUG_OSCILLOSCOPE) {
+		if (gen->ym->scope) {
+			oscilloscope *scope = gen->ym->scope;
+			gen->ym->scope = NULL;
+			gen->psg->scope = NULL;
+			if (gen->expansion) {
+				segacd_context *cd = gen->expansion;
+				cd->pcm.scope = NULL;
+			}
+			scope_close(scope);
+		} else {
+			oscilloscope *scope = create_oscilloscope();
+			ym_enable_scope(gen->ym, scope);
+			psg_enable_scope(gen->psg, scope);
+			if (gen->expansion) {
+				segacd_context *cd = gen->expansion;
+				rf5c164_enable_scope(&cd->pcm, scope);
+			}
+		}
+	}
+}
+
 static void *tmss_rom_write_16(uint32_t address, void *context, uint16_t value)
 {
 	m68k_context *m68k = context;
@@ -1886,6 +1916,7 @@
 	gen->header.deserialize = deserialize;
 	gen->header.start_vgm_log = start_vgm_log;
 	gen->header.stop_vgm_log = stop_vgm_log;
+	gen->header.toggle_debug_view = toggle_debug_view;
 	gen->header.type = SYSTEM_GENESIS;
 	gen->header.info = *rom;
 	set_region(gen, rom, force_region);
--- a/psg.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/psg.c	Tue Nov 22 17:57:02 2022 -0800
@@ -21,6 +21,21 @@
 	context->pan = 0xFF;
 }
 
+void psg_enable_scope(psg_context *context, oscilloscope *scope)
+{
+	context->scope = scope;
+	static const char *names[] = {
+		"PSG #1",
+		"PSG #2",
+		"PSG #3",
+		"PSG Noise",
+	};
+	for (int i = 0; i < 4; i++)
+	{
+		context->scope_channel[i] = scope_add_channel(scope, names[i], 53693175 / context->clock_inc);
+	}
+}
+
 void psg_free(psg_context *context)
 {
 	render_free_source(context->audio);
@@ -91,6 +106,7 @@
 void psg_run(psg_context * context, uint32_t cycles)
 {
 	while (context->cycles < cycles) {
+		uint8_t trigger[4] = {0,0,0,0};
 		for (int i = 0; i < 4; i++) {
 			if (context->counters[i]) {
 				context->counters[i] -= 1;
@@ -98,6 +114,7 @@
 			if (!context->counters[i]) {
 				context->counters[i] = context->counter_load[i];
 				context->output_state[i] = !context->output_state[i];
+				trigger[i] = context->output_state[i];
 				if (i == 3 && context->output_state[i]) {
 					context->noise_out = context->lsfr & 1;
 					context->lsfr = (context->lsfr >> 1) | (context->lsfr << 15);
@@ -114,9 +131,10 @@
 		int16_t left_accum = 0, right_accum = 0;
 		uint8_t pan_left = 0x10, pan_right = 0x1;
 
+		int16_t value = 0;
 		for (int i = 0; i < 3; i++) {
 			if (context->output_state[i]) {
-				int16_t value = volume_table[context->volume[i]];
+				value = volume_table[context->volume[i]];
 				if (context->pan & pan_left) {
 					left_accum += value;
 				}
@@ -126,9 +144,13 @@
 				pan_left <<= 1;
 				pan_right <<= 1;
 			}
+			if (context->scope) {
+				scope_add_sample(context->scope, context->scope_channel[i], value, trigger[i]);
+			}
 		}
+		value = 0;
 		if (context->noise_out) {
-			int16_t value = volume_table[context->volume[3]];
+			value = volume_table[context->volume[3]];
 			if (context->pan & pan_left) {
 				left_accum += value;
 			}
@@ -136,6 +158,9 @@
 				right_accum += value;
 			}
 		}
+		if (context->scope) {
+			scope_add_sample(context->scope, context->scope_channel[3], value, trigger[3]);
+		}
 
 		render_put_stereo_sample(context->audio, left_accum, right_accum);
 
--- a/psg.h	Wed Sep 21 23:16:39 2022 -0700
+++ b/psg.h	Tue Nov 22 17:57:02 2022 -0800
@@ -10,10 +10,12 @@
 #include "serialize.h"
 #include "render_audio.h"
 #include "vgm.h"
+#include "oscilloscope.h"
 
 typedef struct {
 	audio_source *audio;
 	vgm_writer   *vgm;
+	oscilloscope *scope;
 	uint32_t clock_inc;
 	uint32_t cycles;
 	uint16_t lsfr;
@@ -21,6 +23,7 @@
 	uint16_t counters[4];
 	uint8_t  volume[4];
 	uint8_t  output_state[4];
+	uint8_t  scope_channel[4];
 	uint8_t  noise_out;
 	uint8_t  noise_use_tone;
 	uint8_t  noise_type;
@@ -30,6 +33,7 @@
 
 
 void psg_init(psg_context * context, uint32_t master_clock, uint32_t clock_div);
+void psg_enable_scope(psg_context *context, oscilloscope *scope);
 void psg_free(psg_context *context);
 void psg_adjust_master_clock(psg_context * context, uint32_t master_clock);
 void psg_write(psg_context * context, uint8_t value);
--- a/rf5c164.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/rf5c164.c	Tue Nov 22 17:57:02 2022 -0800
@@ -92,10 +92,15 @@
 				pcm->channels[pcm->cur_channel].cur_ptr = pcm->channels[pcm->cur_channel].regs[ST] << 19;
 				//printf("chan %d START %X (%X raw)\n", pcm->cur_channel, pcm->channels[pcm->cur_channel].cur_ptr >> 11, pcm->channels[pcm->cur_channel].cur_ptr);
 				pcm->channels[pcm->cur_channel].state = NORMAL;
+				pcm->channels[pcm->cur_channel].trigger = 1;
 			} else if (pcm->channels[pcm->cur_channel].state == LOOP) {
+				uint32_t old_ptr = pcm->channels[pcm->cur_channel].cur_ptr;
 				pcm->channels[pcm->cur_channel].cur_ptr = (pcm->channels[pcm->cur_channel].regs[LSH] << 19) | (pcm->channels[pcm->cur_channel].regs[LSL] << 11);
 				//printf("chan %d LOOP %X (%X raw)\n", pcm->cur_channel, pcm->channels[pcm->cur_channel].cur_ptr >> 11, pcm->channels[pcm->cur_channel].cur_ptr);
 				pcm->channels[pcm->cur_channel].state = NORMAL;
+				pcm->channels[pcm->cur_channel].trigger = old_ptr != pcm->channels[pcm->cur_channel].cur_ptr;
+			} else {
+				pcm->channels[pcm->cur_channel].trigger = 0;
 			}
 		}
 		write_if_not_sounding(pcm);
@@ -144,8 +149,13 @@
 			int16_t left = (sample * (pcm->channels[pcm->cur_channel].regs[PAN] >> 4)) >> 5;
 			int16_t right = (sample * (pcm->channels[pcm->cur_channel].regs[PAN] & 0xF)) >> 5;
 			//printf("chan %d, raw %X, sample %d, left %d, right %d, ptr %X (raw %X)\n", pcm->cur_channel, pcm->channels[pcm->cur_channel].sample, sample, left, right, pcm->channels[pcm->cur_channel].cur_ptr >> 11, pcm->channels[pcm->cur_channel].cur_ptr);
+			if (pcm->scope) {
+				scope_add_sample(pcm->scope, pcm->channels[pcm->cur_channel].scope_channel, sample, pcm->channels[pcm->cur_channel].trigger);
+			}
 			pcm->left += left;
 			pcm->right += right;
+		} else if (pcm->scope) {
+			scope_add_sample(pcm->scope, pcm->channels[pcm->cur_channel].scope_channel, 0, 0);
 		}
 		write_if_not_sounding(pcm);
 		CHECK;
@@ -228,3 +238,22 @@
 		return 0xFF;
 	}
 }
+
+void rf5c164_enable_scope(rf5c164* pcm, oscilloscope *scope)
+{
+	static const char *names[] = {
+		"Richo #1",
+		"Richo #2",
+		"Richo #3",
+		"Richo #4",
+		"Richo #5",
+		"Richo #6",
+		"Richo #7",
+		"Richo #8",
+	};
+	pcm->scope = scope;
+	for (int i = 0; i < 8; i ++)
+	{
+		pcm->channels[i].scope_channel = scope_add_channel(scope, names[i], 50000000 / (pcm->clock_step * 96));
+	}
+}
--- a/rf5c164.h	Wed Sep 21 23:16:39 2022 -0700
+++ b/rf5c164.h	Tue Nov 22 17:57:02 2022 -0800
@@ -2,16 +2,20 @@
 #define RF5C164_H_
 #include <stdint.h>
 #include "render_audio.h"
+#include "oscilloscope.h"
 
 typedef struct {
 	uint32_t cur_ptr;
 	uint8_t  regs[7];
 	uint8_t  sample;
 	uint8_t  state;
+	uint8_t  scope_channel;
+	uint8_t  trigger;
 } rf5c164_channel;
 
 typedef struct {
 	audio_source    *audio;
+	oscilloscope    *scope;
 	uint32_t        cycle;
 	uint32_t        clock_step;
 	uint16_t        ram[64*1024];
@@ -33,5 +37,6 @@
 void rf5c164_run(rf5c164* pcm, uint32_t cycle);
 void rf5c164_write(rf5c164* pcm, uint16_t address, uint8_t value);
 uint8_t rf5c164_read(rf5c164* pcm, uint16_t address);
+void rf5c164_enable_scope(rf5c164* pcm, oscilloscope *scope);
 
 #endif //RF5C164_H_
--- a/sms.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/sms.c	Tue Nov 22 17:57:02 2022 -0800
@@ -426,6 +426,9 @@
 
 		}
 		if (sms->vdp->frame != sms->last_frame) {
+			if (sms->psg->scope) {
+				scope_render(sms->psg->scope);
+			}
 			uint32_t elapsed = sms->vdp->frame - sms->last_frame;
 			sms->last_frame = sms->vdp->frame;
 			if (system->enter_debugger_frames) {
@@ -648,6 +651,22 @@
 	setup_io_devices(config, &system->info, &sms->io);
 }
 
+static void toggle_debug_view(system_header *system, uint8_t debug_view)
+{
+	sms_context *sms = (sms_context *)system;
+	if (debug_view < DEBUG_OSCILLOSCOPE) {
+		vdp_toggle_debug_view(sms->vdp, debug_view);
+	} else if (debug_view == DEBUG_OSCILLOSCOPE) {
+		if (sms->psg->scope) {
+			oscilloscope *scope = sms->psg->scope;
+			sms->psg->scope = NULL;
+			scope_close(scope);
+		} else {
+			oscilloscope *scope = create_oscilloscope();
+			psg_enable_scope(sms->psg, scope);
+		}
+	}
+}
 
 sms_context *alloc_configure_sms(system_media *media, uint32_t opts, uint8_t force_region)
 {
@@ -750,6 +769,7 @@
 	sms->header.config_updated = config_updated;
 	sms->header.serialize = serialize;
 	sms->header.deserialize = deserialize;
+	sms->header.toggle_debug_view = toggle_debug_view;
 	sms->header.type = SYSTEM_SMS;
 
 	return sms;
--- a/system.h	Wed Sep 21 23:16:39 2022 -0700
+++ b/system.h	Tue Nov 22 17:57:02 2022 -0800
@@ -22,6 +22,15 @@
 	DEBUGGER_GDB
 } debugger_type;
 
+enum {
+	DEBUG_PLANE,
+	DEBUG_VRAM,
+	DEBUG_CRAM,
+	DEBUG_COMPOSITE,
+	DEBUG_OSCILLOSCOPE,
+	NUM_DEBUG_TYPES
+};
+
 typedef void (*system_fun)(system_header *);
 typedef uint16_t (*system_fun_r16)(system_header *);
 typedef void (*system_str_fun)(system_header *, char *);
@@ -65,6 +74,7 @@
 	system_ptr8_sizet_fun   deserialize;
 	system_str_fun          start_vgm_log;
 	system_fun              stop_vgm_log;
+	system_u8_fun           toggle_debug_view;
 	rom_info          info;
 	arena             *arena;
 	char              *next_rom;
--- a/vdp.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/vdp.c	Tue Nov 22 17:57:02 2022 -0800
@@ -264,7 +264,7 @@
 	if (headless) {
 		free(context->fb);
 	}
-	for (int i = 0; i < VDP_NUM_DEBUG_TYPES; i++)
+	for (int i = 0; i < NUM_DEBUG_TYPES; i++)
 	{
 		if (context->enabled_debuggers & (1 << i)) {
 			vdp_toggle_debug_view(context, i);
@@ -1900,7 +1900,7 @@
 		jump_end = 0x1D5;
 	}
 
-	if (context->enabled_debuggers & (1 << VDP_DEBUG_CRAM | 1 << VDP_DEBUG_COMPOSITE)) {
+	if (context->enabled_debuggers & (1 << DEBUG_CRAM | 1 << DEBUG_COMPOSITE)) {
 		uint32_t line = context->vcounter;
 		if (line >= jump_end) {
 			line -= jump_end - jump_start;
@@ -1912,8 +1912,8 @@
 		} else {
 			line += context->border_top;
 		}
-		if (context->enabled_debuggers & (1 << VDP_DEBUG_CRAM)) {
-			uint32_t *fb = context->debug_fbs[VDP_DEBUG_CRAM] + context->debug_fb_pitch[VDP_DEBUG_CRAM] * line / sizeof(uint32_t);
+		if (context->enabled_debuggers & (1 << DEBUG_CRAM)) {
+			uint32_t *fb = context->debug_fbs[DEBUG_CRAM] + context->debug_fb_pitch[DEBUG_CRAM] * line / sizeof(uint32_t);
 			if (context->regs[REG_MODE_2] & BIT_MODE_5) {
 				for (int i = 0; i < 64; i++)
 				{
@@ -1933,10 +1933,10 @@
 			}
 		}
 		if (
-			context->enabled_debuggers & (1 << VDP_DEBUG_COMPOSITE)
+			context->enabled_debuggers & (1 << DEBUG_COMPOSITE)
 			&& line < (context->inactive_start + context->border_bot + context->border_top)
 		) {
-			uint32_t *fb = context->debug_fbs[VDP_DEBUG_COMPOSITE] + context->debug_fb_pitch[VDP_DEBUG_COMPOSITE] * line / sizeof(uint32_t);
+			uint32_t *fb = context->debug_fbs[DEBUG_COMPOSITE] + context->debug_fb_pitch[DEBUG_COMPOSITE] * line / sizeof(uint32_t);
 			for (int i = 0; i < LINEBUF_SIZE; i++)
 			{
 				*(fb++) = context->debugcolors[context->layer_debug_buf[i]];
@@ -1970,9 +1970,9 @@
 
 static void vdp_update_per_frame_debug(vdp_context *context)
 {
-	if (context->enabled_debuggers & (1 << VDP_DEBUG_PLANE)) {
+	if (context->enabled_debuggers & (1 << DEBUG_PLANE)) {
 		uint32_t pitch;
-		uint32_t *fb = render_get_framebuffer(context->debug_fb_indices[VDP_DEBUG_PLANE], &pitch);
+		uint32_t *fb = render_get_framebuffer(context->debug_fb_indices[DEBUG_PLANE], &pitch);
 		uint16_t hscroll_mask;
 		uint16_t v_mul;
 		uint16_t vscroll_mask = 0x1F | (context->regs[REG_SCROLL] & 0x30) << 1;
@@ -1997,7 +1997,7 @@
 			break;
 		}
 		uint16_t table_address;
-		switch(context->debug_modes[VDP_DEBUG_PLANE] % 3)
+		switch(context->debug_modes[DEBUG_PLANE] % 3)
 		{
 		case 0:
 			table_address = context->regs[REG_SCROLL_A] << 10 & 0xE000;
@@ -2067,14 +2067,14 @@
 				}
 			}
 		}
-		render_framebuffer_updated(context->debug_fb_indices[VDP_DEBUG_PLANE], 1024);
+		render_framebuffer_updated(context->debug_fb_indices[DEBUG_PLANE], 1024);
 	}
 
-	if (context->enabled_debuggers & (1 << VDP_DEBUG_VRAM)) {
+	if (context->enabled_debuggers & (1 << DEBUG_VRAM)) {
 		uint32_t pitch;
-		uint32_t *fb = render_get_framebuffer(context->debug_fb_indices[VDP_DEBUG_VRAM], &pitch);
+		uint32_t *fb = render_get_framebuffer(context->debug_fb_indices[DEBUG_VRAM], &pitch);
 
-		uint8_t pal = (context->debug_modes[VDP_DEBUG_VRAM] % 4) << 4;
+		uint8_t pal = (context->debug_modes[DEBUG_VRAM] % 4) << 4;
 		for (int y = 0; y < 512; y++)
 		{
 			uint32_t *line = fb + y * pitch / sizeof(uint32_t);
@@ -2096,13 +2096,13 @@
 			}
 		}
 
-		render_framebuffer_updated(context->debug_fb_indices[VDP_DEBUG_VRAM], 1024);
+		render_framebuffer_updated(context->debug_fb_indices[DEBUG_VRAM], 1024);
 	}
 
-	if (context->enabled_debuggers & (1 << VDP_DEBUG_CRAM)) {
+	if (context->enabled_debuggers & (1 << DEBUG_CRAM)) {
 		uint32_t starting_line = 512 - 32*4;
-		uint32_t *line = context->debug_fbs[VDP_DEBUG_CRAM]
-			+ context->debug_fb_pitch[VDP_DEBUG_CRAM]  * starting_line / sizeof(uint32_t);
+		uint32_t *line = context->debug_fbs[DEBUG_CRAM]
+			+ context->debug_fb_pitch[DEBUG_CRAM]  * starting_line / sizeof(uint32_t);
 		if (context->regs[REG_MODE_2] & BIT_MODE_5) {
 			for (int pal = 0; pal < 4; pal ++)
 			{
@@ -2118,14 +2118,14 @@
 						}
 						*(cur++) = 0xFF000000;
 					}
-					line += context->debug_fb_pitch[VDP_DEBUG_CRAM] / sizeof(uint32_t);
+					line += context->debug_fb_pitch[DEBUG_CRAM] / sizeof(uint32_t);
 				}
 				cur = line;
 				for (int x = 0; x < 512; x++)
 				{
 					*(cur++) = 0xFF000000;
 				}
-				line += context->debug_fb_pitch[VDP_DEBUG_CRAM] / sizeof(uint32_t);
+				line += context->debug_fb_pitch[DEBUG_CRAM] / sizeof(uint32_t);
 			}
 		} else {
 			for (int pal = 0; pal < 2; pal ++)
@@ -2142,22 +2142,22 @@
 						}
 						*(cur++) = 0xFF000000;
 					}
-					line += context->debug_fb_pitch[VDP_DEBUG_CRAM] / sizeof(uint32_t);
+					line += context->debug_fb_pitch[DEBUG_CRAM] / sizeof(uint32_t);
 				}
 				cur = line;
 				for (int x = 0; x < 512; x++)
 				{
 					*(cur++) = 0xFF000000;
 				}
-				line += context->debug_fb_pitch[VDP_DEBUG_CRAM] / sizeof(uint32_t);
+				line += context->debug_fb_pitch[DEBUG_CRAM] / sizeof(uint32_t);
 			}
 		}
-		render_framebuffer_updated(context->debug_fb_indices[VDP_DEBUG_CRAM], 512);
-		context->debug_fbs[VDP_DEBUG_CRAM] = render_get_framebuffer(context->debug_fb_indices[VDP_DEBUG_CRAM], &context->debug_fb_pitch[VDP_DEBUG_CRAM]);
-	}
-	if (context->enabled_debuggers & (1 << VDP_DEBUG_COMPOSITE)) {
-		render_framebuffer_updated(context->debug_fb_indices[VDP_DEBUG_COMPOSITE], LINEBUF_SIZE);
-		context->debug_fbs[VDP_DEBUG_COMPOSITE] = render_get_framebuffer(context->debug_fb_indices[VDP_DEBUG_COMPOSITE], &context->debug_fb_pitch[VDP_DEBUG_COMPOSITE]);
+		render_framebuffer_updated(context->debug_fb_indices[DEBUG_CRAM], 512);
+		context->debug_fbs[DEBUG_CRAM] = render_get_framebuffer(context->debug_fb_indices[DEBUG_CRAM], &context->debug_fb_pitch[DEBUG_CRAM]);
+	}
+	if (context->enabled_debuggers & (1 << DEBUG_COMPOSITE)) {
+		render_framebuffer_updated(context->debug_fb_indices[DEBUG_COMPOSITE], LINEBUF_SIZE);
+		context->debug_fbs[DEBUG_COMPOSITE] = render_get_framebuffer(context->debug_fb_indices[DEBUG_COMPOSITE], &context->debug_fb_pitch[DEBUG_COMPOSITE]);
 	}
 }
 
@@ -4655,7 +4655,7 @@
 static void vdp_debug_window_close(uint8_t which)
 {
 	//TODO: remove need for current_vdp global, and find the VDP via current_system instead
-	for (int i = 0; i < VDP_NUM_DEBUG_TYPES; i++)
+	for (int i = 0; i < NUM_DEBUG_TYPES; i++)
 	{
 		if (current_vdp->enabled_debuggers & (1 << i) && which == current_vdp->debug_fb_indices[i]) {
 			vdp_toggle_debug_view(current_vdp, i);
@@ -4675,22 +4675,22 @@
 		char *caption;
 		switch(debug_type)
 		{
-		case VDP_DEBUG_PLANE:
+		case DEBUG_PLANE:
 			caption = "BlastEm - VDP Plane Debugger";
 			width = height = 1024;
 			break;
-		case VDP_DEBUG_VRAM:
+		case DEBUG_VRAM:
 			caption = "BlastEm - VDP VRAM Debugger";
 			width = 1024;
 			height = 512;
 			break;
-		case VDP_DEBUG_CRAM:
+		case DEBUG_CRAM:
 			caption = "BlastEm - VDP CRAM Debugger";
 			width = 512;
 			height = 512;
 			fetch_immediately = 1;
 			break;
-		case VDP_DEBUG_COMPOSITE:
+		case DEBUG_COMPOSITE:
 			caption = "BlastEm - VDP Plane Composition Debugger";
 			width = LINEBUF_SIZE;
 			height = context->inactive_start + context->border_top + context->border_bot;
@@ -4716,7 +4716,7 @@
 	if (active < FRAMEBUFFER_USER_START) {
 		return;
 	}
-	for (int i = 0; i < VDP_NUM_DEBUG_TYPES; i++)
+	for (int i = 0; i < NUM_DEBUG_TYPES; i++)
 	{
 		if (context->enabled_debuggers & (1 << i) && context->debug_fb_indices[i] == active) {
 			context->debug_modes[i]++;
--- a/vdp.h	Wed Sep 21 23:16:39 2022 -0700
+++ b/vdp.h	Tue Nov 22 17:57:02 2022 -0800
@@ -157,14 +157,6 @@
 } fifo_entry;
 
 enum {
-	VDP_DEBUG_PLANE,
-	VDP_DEBUG_VRAM,
-	VDP_DEBUG_CRAM,
-	VDP_DEBUG_COMPOSITE,
-	VDP_NUM_DEBUG_TYPES
-};
-
-enum {
 	VDP_GENESIS,
 	VDP_GAMEGEAR,
 	VDP_SMS2,
@@ -179,13 +171,13 @@
 	//pointer to current framebuffer
 	uint32_t       *fb;
 	uint8_t        *done_composite;
-	uint32_t       *debug_fbs[VDP_NUM_DEBUG_TYPES];
+	uint32_t       *debug_fbs[NUM_DEBUG_TYPES];
 	char           *kmod_msg_buffer;
 	uint32_t       kmod_buffer_storage;
 	uint32_t       kmod_buffer_length;
 	uint32_t       timer_start_cycle;
 	uint32_t       output_pitch;
-	uint32_t       debug_fb_pitch[VDP_NUM_DEBUG_TYPES];
+	uint32_t       debug_fb_pitch[NUM_DEBUG_TYPES];
 	fifo_entry     fifo[FIFO_SIZE];
 	int32_t        fifo_write;
 	int32_t        fifo_read;
@@ -251,8 +243,8 @@
 	uint8_t        tmp_buf_a[SCROLL_BUFFER_SIZE];
 	uint8_t        tmp_buf_b[SCROLL_BUFFER_SIZE];
 	uint8_t        enabled_debuggers;
-	uint8_t        debug_fb_indices[VDP_NUM_DEBUG_TYPES];
-	uint8_t        debug_modes[VDP_NUM_DEBUG_TYPES];
+	uint8_t        debug_fb_indices[NUM_DEBUG_TYPES];
+	uint8_t        debug_modes[NUM_DEBUG_TYPES];
 	uint8_t        pushed_frame;
 	uint8_t        type;
 	uint8_t        cram_latch;
--- a/ym2612.c	Wed Sep 21 23:16:39 2022 -0700
+++ b/ym2612.c	Tue Nov 22 17:57:02 2022 -0800
@@ -156,9 +156,11 @@
 	memset(context->part2_regs, 0, sizeof(context->part2_regs));
 	memset(context->operators, 0, sizeof(context->operators));
 	FILE* savedlogs[NUM_CHANNELS];
+	uint8_t saved_scope_channel[NUM_CHANNELS];
 	for (int i = 0; i < NUM_CHANNELS; i++)
 	{
 		savedlogs[i] = context->channels[i].logfile;
+		saved_scope_channel[i] = context->channels[i].scope_channel;
 	}
 	memset(context->channels, 0, sizeof(context->channels));
 	memset(context->ch3_supp, 0, sizeof(context->ch3_supp));
@@ -179,6 +181,7 @@
 	for (int i = 0; i < NUM_CHANNELS; i++) {
 		context->channels[i].lr = 0xC0;
 		context->channels[i].logfile = savedlogs[i];
+		context->channels[i].scope_channel = saved_scope_channel[i];
 		if (i < 3) {
 			context->part1_regs[REG_LR_AMS_PMS - YM_PART1_START + i] = 0xC0;
 		} else {
@@ -496,7 +499,9 @@
 		ym_operator * operator = context->operators + op;
 		ym_channel * chan = context->channels + channel;
 		uint16_t phase = operator->phase_counter >> 10 & 0x3FF;
+		uint32_t old_phase = operator->phase_counter;
 		operator->phase_counter += operator->phase_inc;//ym_calc_phase_inc(context, operator, op);
+		operator->phase_overflow = (old_phase & 0xFFFFF) > (operator->phase_counter & 0xFFFFF);
 		int16_t mod = 0;
 		if (op & 3) {
 			if (operator->mod_src[0]) {
@@ -568,21 +573,27 @@
 		if (op % 4 == 3) {
 			if (chan->algorithm < 4) {
 				chan->output = operator->output;
+				chan->phase_overflow = operator->phase_overflow;
 			} else if(chan->algorithm == 4) {
-				chan->output = operator->output + context->operators[channel * 4 + 2].output;
+				ym_operator *other_op = context->operators + channel * 4 + 2;
+				chan->output = operator->output + other_op->output;
+				if (operator->phase_inc < other_op->phase_inc) {
+					chan->phase_overflow = operator->phase_overflow;
+				} else {
+					chan->phase_overflow = other_op->phase_overflow;
+				}
 			} else {
 				output = 0;
+				uint32_t lowest_phase_inc = 0xFFFFFFFF;
 				for (uint32_t op = ((chan->algorithm == 7) ? 0 : 1) + channel*4; op < (channel+1)*4; op++) {
 					output += context->operators[op].output;
+					if (context->operators[op].phase_inc < lowest_phase_inc) {
+						lowest_phase_inc = context->operators[op].phase_inc;
+						chan->phase_overflow = context->operators[op].phase_overflow;
+					}
 				}
 				chan->output = output;
 			}
-			if (first_key_on) {
-				int16_t value = context->channels[channel].output & 0x3FE0;
-				if (value & 0x2000) {
-					value |= 0xC000;
-				}
-			}
 		}
 		//puts("operator update done");
 	}
@@ -611,6 +622,9 @@
 		if (context->channels[i].logfile) {
 			fwrite(&value, sizeof(value), 1, context->channels[i].logfile);
 		}
+		if (context->scope) {
+			scope_add_sample(context->scope, context->channels[i].scope_channel, value, context->channels[i].phase_overflow);
+		}
 		if (context->channels[i].lr & 0x80) {
 			left += (value * context->volume_mult) / context->volume_div;
 		} else if (context->zero_offset) {
@@ -1382,3 +1396,21 @@
 		context->last_status_cycle = context->write_cycle;
 	}
 }
+
+void ym_enable_scope(ym2612_context *context, oscilloscope *scope)
+{
+	static const char *names[] = {
+		"YM2612 #1",
+		"YM2612 #2",
+		"YM2612 #3",
+		"YM2612 #4",
+		"YM2612 #5",
+		"YM2612 #6"
+	};
+	context->scope = scope;
+	for (int i = 0; i < NUM_CHANNELS; i++)
+	{
+		//TODO: calculate actual sample rate based on current clock settings
+		context->channels[i].scope_channel = scope_add_channel(scope, names[i], 53267);
+	}
+}
--- a/ym2612.h	Wed Sep 21 23:16:39 2022 -0700
+++ b/ym2612.h	Tue Nov 22 17:57:02 2022 -0800
@@ -11,6 +11,7 @@
 #include "serialize.h"
 #include "render_audio.h"
 #include "vgm.h"
+#include "oscilloscope.h"
 
 #define NUM_PART_REGS (0xB7-0x30)
 #define NUM_CHANNELS 6
@@ -35,6 +36,7 @@
 	uint8_t  env_phase;
 	uint8_t  ssg;
 	uint8_t  inverted;
+	uint8_t  phase_overflow;
 } ym_operator;
 
 typedef struct {
@@ -52,6 +54,8 @@
 	uint8_t  pms;
 	uint8_t  lr;
 	uint8_t  keyon;
+	uint8_t  scope_channel;
+	uint8_t  phase_overflow;
 } ym_channel;
 
 typedef struct {
@@ -69,45 +73,46 @@
 
 typedef struct {
 	audio_source *audio;
-	vgm_writer  *vgm;
-    uint32_t    clock_inc;
-	uint32_t    current_cycle;
-	uint32_t    write_cycle;
-	uint32_t    busy_start;
-	uint32_t    busy_cycles;
-	uint32_t    last_status_cycle;
-	uint32_t    invalid_status_decay;
-	uint32_t    status_address_mask;
-	int32_t     volume_mult;
-	int32_t     volume_div;
-	ym_operator operators[NUM_OPERATORS];
-	ym_channel  channels[NUM_CHANNELS];
-	int16_t     zero_offset;
-	uint16_t    timer_a;
-	uint16_t    timer_a_load;
-	uint16_t    env_counter;
-	ym_supp     ch3_supp[3];
-	uint8_t     timer_b;
-	uint8_t     sub_timer_b;
-	uint8_t     timer_b_load;
-	uint8_t     ch3_mode;
-	uint8_t     current_op;
-	uint8_t     current_env_op;
+	vgm_writer   *vgm;
+	oscilloscope *scope;
+    uint32_t     clock_inc;
+	uint32_t     current_cycle;
+	uint32_t     write_cycle;
+	uint32_t     busy_start;
+	uint32_t     busy_cycles;
+	uint32_t     last_status_cycle;
+	uint32_t     invalid_status_decay;
+	uint32_t     status_address_mask;
+	int32_t      volume_mult;
+	int32_t      volume_div;
+	ym_operator  operators[NUM_OPERATORS];
+	ym_channel   channels[NUM_CHANNELS];
+	int16_t      zero_offset;
+	uint16_t     timer_a;
+	uint16_t     timer_a_load;
+	uint16_t     env_counter;
+	ym_supp      ch3_supp[3];
+	uint8_t      timer_b;
+	uint8_t      sub_timer_b;
+	uint8_t      timer_b_load;
+	uint8_t      ch3_mode;
+	uint8_t      current_op;
+	uint8_t      current_env_op;
 
-	uint8_t     timer_control;
-	uint8_t     dac_enable;
-	uint8_t     lfo_enable;
-	uint8_t     lfo_freq;
-	uint8_t     lfo_counter;
-	uint8_t     lfo_am_step;
-	uint8_t     lfo_pm_step;
-	uint8_t     csm_keyon;
-	uint8_t     status;
-	uint8_t     last_status;
-	uint8_t     selected_reg;
-	uint8_t     selected_part;
-	uint8_t     part1_regs[YM_PART1_REGS];
-	uint8_t     part2_regs[YM_PART2_REGS];
+	uint8_t      timer_control;
+	uint8_t      dac_enable;
+	uint8_t      lfo_enable;
+	uint8_t      lfo_freq;
+	uint8_t      lfo_counter;
+	uint8_t      lfo_am_step;
+	uint8_t      lfo_pm_step;
+	uint8_t      csm_keyon;
+	uint8_t      status;
+	uint8_t      last_status;
+	uint8_t      selected_reg;
+	uint8_t      selected_part;
+	uint8_t      part1_regs[YM_PART1_REGS];
+	uint8_t      part2_regs[YM_PART2_REGS];
 } ym2612_context;
 
 enum {
@@ -154,6 +159,7 @@
 void ym_print_timer_info(ym2612_context *context);
 void ym_serialize(ym2612_context *context, serialize_buffer *buf);
 void ym_deserialize(deserialize_buffer *buf, void *vcontext);
+void ym_enable_scope(ym2612_context *context, oscilloscope *scope);
 
 #endif //YM2612_H_