changeset 2693:46dba737b931

WIP Nuklear UI in VDP debug windows
author Michael Pavone <pavone@retrodev.com>
date Thu, 19 Jun 2025 19:59:05 -0700
parents effbb52ab3f0
children a6a27d4aa8ab
files Makefile nuklear_ui/blastem_nuklear.c nuklear_ui/blastem_nuklear.h nuklear_ui/debug_ui.c nuklear_ui/debug_ui.h nuklear_ui/nuklear_sdl_gles2.h render.h render_sdl.c render_sdl.h vdp.c
diffstat 10 files changed, 392 insertions(+), 118 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Jun 15 15:46:34 2025 -0700
+++ b/Makefile	Thu Jun 19 19:59:05 2025 -0700
@@ -266,7 +266,7 @@
 endif
 AUDIOOBJS=ym2612.o ymf262.o ym_common.o psg.o wave.o flac.o vgm.o event_log.o render_audio.o rf5c164.o
 CONFIGOBJS=config.o tern.o util.o paths.o
-NUKLEAROBJS=$(FONT) $(CHOOSER) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o
+NUKLEAROBJS=$(FONT) $(CHOOSER) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o nuklear_ui/debug_ui.o
 RENDEROBJS=ppm.o controller_info.o
 ifdef USE_FBDEV
 RENDEROBJS+= render_fbdev.o
--- a/nuklear_ui/blastem_nuklear.c	Sun Jun 15 15:46:34 2025 -0700
+++ b/nuklear_ui/blastem_nuklear.c	Thu Jun 19 19:59:05 2025 -0700
@@ -2626,7 +2626,7 @@
 			render_framebuffer_updated(FRAMEBUFFER_UI, render_width());
 		} else {
 #ifndef DISABLE_OPENGL
-			nk_sdl_render(NK_ANTI_ALIASING_ON, 512 * 1024, 128 * 1024);
+			nk_sdl_render(context, NK_ANTI_ALIASING_ON, 512 * 1024, 128 * 1024);
 #endif
 		}
 		nk_input_begin(context);
@@ -2668,7 +2668,7 @@
 	ui_exit();
 #endif
 }
-static void handle_event(SDL_Event *event)
+static void handle_event(uint8_t which, SDL_Event *event)
 {
 	SDL_Joystick *joy = render_get_joystick(selected_controller);
 	if (event->type == SDL_KEYDOWN) {
@@ -2695,14 +2695,14 @@
 	if (stick_nav_disabled && event->type == SDL_CONTROLLERAXISMOTION) {
 		return;
 	}
-	nk_sdl_handle_event(event);
+	nk_sdl_handle_event(context, event);
 }
 
 static void context_destroyed(void)
 {
 	if (context)
 	{
-		nk_sdl_shutdown();
+		nk_sdl_shutdown(context);
 		context = NULL;
 	}
 }
@@ -2737,14 +2737,14 @@
 	return nk_image_ptr(fbimg);
 }
 
-static void texture_init(void)
+static void font_init(struct nk_context *ctx)
 {
 	struct nk_font_atlas *atlas;
 	if (fb_context) {
 		nk_rawfb_font_stash_begin(fb_context, &atlas);
 	} else {
 #ifndef DISABLE_OPENGL
-		nk_sdl_font_stash_begin(&atlas);
+		nk_sdl_font_stash_begin(ctx, &atlas);
 #endif
 	}
 	uint32_t font_size;
@@ -2758,10 +2758,14 @@
 		nk_rawfb_font_stash_end(fb_context);
 	} else {
 #ifndef DISABLE_OPENGL
-		nk_sdl_font_stash_end();
+		nk_sdl_font_stash_end(ctx);
 #endif
 	}
-	nk_style_set_font(context, &def_font->handle);
+	nk_style_set_font(ctx, &def_font->handle);
+}
+
+static void texture_init(void)
+{
 	for (uint32_t i = 0; i < num_ui_images; i++)
 	{
 #ifndef DISABLE_OPENGL
@@ -2776,44 +2780,33 @@
 	}
 }
 
-static void style_init(void)
+static void style_init(struct nk_context *ctx)
 {
-	context->style.checkbox.padding.x = render_height() / 120;
-	context->style.checkbox.padding.y = render_height() / 120;
-	context->style.checkbox.border = render_height() / 240;
-	context->style.checkbox.cursor_normal.type = NK_STYLE_ITEM_COLOR;
-	context->style.checkbox.cursor_normal.data.color = (struct nk_color){
+	ctx->style.checkbox.padding.x = render_height() / 120;
+	ctx->style.checkbox.padding.y = render_height() / 120;
+	ctx->style.checkbox.border = render_height() / 240;
+	ctx->style.checkbox.cursor_normal.type = NK_STYLE_ITEM_COLOR;
+	ctx->style.checkbox.cursor_normal.data.color = (struct nk_color){
 		.r = 255, .g = 128, .b = 0, .a = 255
 	};
-	context->style.checkbox.cursor_hover = context->style.checkbox.cursor_normal;
-	context->style.property.inc_button.text_hover = (struct nk_color){
+	ctx->style.checkbox.cursor_hover = ctx->style.checkbox.cursor_normal;
+	ctx->style.property.inc_button.text_hover = (struct nk_color){
 		.r = 255, .g = 128, .b = 0, .a = 255
 	};
-	context->style.property.dec_button.text_hover = context->style.property.inc_button.text_hover;
-	context->style.combo.button.text_hover = context->style.property.inc_button.text_hover;
+	ctx->style.property.dec_button.text_hover = ctx->style.property.inc_button.text_hover;
+	ctx->style.combo.button.text_hover = ctx->style.property.inc_button.text_hover;
 }
 
 static void fb_resize(void)
 {
 	nk_rawfb_resize_fb(fb_context, NULL, render_width(), render_height(), 0);
-	style_init();
+	style_init(context);
 	texture_init();
 }
 
 static void context_created(void)
 {
-	context = nk_sdl_init(render_get_window());
-#ifndef DISABLE_OPENGL
-	if (render_has_gl()) {
-		nk_sdl_device_create();
-	} else {
-#endif
-		fb_context = nk_rawfb_init(NULL, context, render_width(), render_height(), 0);
-		render_set_ui_fb_resize_handler(fb_resize);
-#ifndef DISABLE_OPENGL
-	}
-#endif
-	style_init();
+	context = shared_nuklear_init(FRAMEBUFFER_UI);
 	texture_init();
 }
 
@@ -2897,12 +2890,21 @@
 	}
 }
 
-void blastem_nuklear_init(uint8_t file_loaded)
+struct nk_context *shared_nuklear_init(uint8_t window)
 {
-	context = nk_sdl_init(render_get_window());
+	if (window >= FRAMEBUFFER_USER_START) {
+#ifdef DISABLE_OPENGL
+		return NULL;
+#else
+		if (!render_has_gl()) {
+			return NULL;
+		}
+#endif
+	}
+	struct nk_context *ret = nk_sdl_init(render_get_window(window));
 #ifndef DISABLE_OPENGL
 	if (render_has_gl()) {
-		nk_sdl_device_create();
+		nk_sdl_device_create(ret);
 	} else {
 #endif
 		fb_context = nk_rawfb_init(NULL, context, render_width(), render_height(), 0);
@@ -2910,7 +2912,14 @@
 #ifndef DISABLE_OPENGL
 	}
 #endif
-	style_init();
+	style_init(ret);
+	font_init(ret);
+	return ret;
+}
+
+void blastem_nuklear_init(uint8_t file_loaded)
+{
+	context = shared_nuklear_init(FRAMEBUFFER_UI);
 
 	controller_360 = load_ui_image("images/360.png");
 	controller_ps4 = load_ui_image("images/ps4.png");
@@ -2926,8 +2935,8 @@
 		current_view = view_menu;
 		set_content_binding_state(0);
 	}
-	render_set_ui_render_fun(blastem_nuklear_render);
-	render_set_event_handler(handle_event);
+	render_set_ui_render_fun(FRAMEBUFFER_UI, blastem_nuklear_render);
+	render_set_event_handler(FRAMEBUFFER_UI, handle_event);
 	render_set_gl_context_handlers(context_destroyed, context_created);
 	char *unf = tern_find_path(config, "ui\0use_native_filechooser\0", TVAL_PTR).ptrval;
 	use_native_filechooser = unf && !strcmp(unf, "on");
--- a/nuklear_ui/blastem_nuklear.h	Sun Jun 15 15:46:34 2025 -0700
+++ b/nuklear_ui/blastem_nuklear.h	Thu Jun 19 19:59:05 2025 -0700
@@ -10,6 +10,7 @@
 #include "nuklear.h"
 #include "nuklear_sdl_gles2.h"
 
+struct nk_context *shared_nuklear_init(uint8_t window);
 void blastem_nuklear_init(uint8_t file_loaded);
 void show_pause_menu(void);
 void show_play_view(void);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuklear_ui/debug_ui.c	Thu Jun 19 19:59:05 2025 -0700
@@ -0,0 +1,162 @@
+#include <limits.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "blastem_nuklear.h"
+#include "../blastem.h"
+#include "../genesis.h"
+#include "../sms.h"
+#include "../coleco.h"
+
+typedef struct {
+	struct nk_context *context;
+	uint32_t          tex_width;
+	uint32_t          tex_height;
+	uint8_t           win_idx;
+} debug_window;
+
+static debug_window windows[NUM_DEBUG_TYPES];
+
+static void debug_handle_event(uint8_t which, SDL_Event *event)
+{
+	for (int i = 0; i < NUM_DEBUG_TYPES; i++)
+	{
+		if (windows[i].win_idx == which) {
+			nk_sdl_handle_event(windows[i].context, event);
+			break;
+		}
+	}
+}
+
+#ifndef DISABLE_OPENGL
+vdp_context *get_vdp(void)
+{
+	if (!current_system) {
+		return NULL;
+	}
+	switch (current_system->type)
+	{
+	case SYSTEM_GENESIS:
+	case SYSTEM_SEGACD:
+	case SYSTEM_PICO:
+	case SYSTEM_COPERA:
+		return ((genesis_context *)current_system)->vdp;
+	case SYSTEM_SMS:
+	case SYSTEM_GAME_GEAR:
+	case SYSTEM_SG1000:
+	case SYSTEM_SC3000:
+		return ((sms_context *)current_system)->vdp;
+	case SYSTEM_COLECOVISION:
+		return ((coleco_context *)current_system)->vdp;
+	default:
+		return NULL;
+	}
+}
+
+void write_cram_internal(vdp_context * context, uint16_t addr, uint16_t value);
+static void cram_debug_ui(void)
+{
+	vdp_context *vdp = get_vdp();
+	if (!vdp) {
+		return;
+	}
+	struct nk_context *context = windows[DEBUG_CRAM].context;
+	nk_input_end(context);
+	char buf[64];
+	
+	struct nk_image main_image = nk_image_id((int)render_get_window_texture(windows[DEBUG_CRAM].win_idx));
+	if (nk_begin(context, "CRAM Debug", nk_rect(0, 0, windows[DEBUG_CRAM].tex_width + 100 + 8, windows[DEBUG_CRAM].tex_height + 8), NK_WINDOW_NO_SCROLLBAR)) {
+		nk_layout_space_begin(context, NK_STATIC, windows[DEBUG_CRAM].tex_height, INT_MAX);
+		nk_layout_space_push(context, nk_rect(100, 0, windows[DEBUG_CRAM].tex_width, windows[DEBUG_CRAM].tex_height));
+		nk_image(context, main_image);
+		struct nk_rect bounds = nk_layout_widget_bounds(context);
+		bounds.x += 100;
+		bounds.w -= 100;
+		bounds.h = (vdp->flags2 & FLAG2_REGION_PAL) ? 313 : 262;
+		if (nk_input_is_mouse_hovering_rect(&context->input, bounds)) {
+			int off_y = context->input.mouse.pos.y - bounds.y;
+			int off_x = context->input.mouse.pos.x - bounds.x;
+			pixel_t pix = vdp->debug_fbs[DEBUG_CRAM][off_y * vdp->debug_fb_pitch[DEBUG_CRAM] / sizeof(pixel_t) + off_x];
+#ifdef USE_GLES
+			pixel_t b = pix >> 20 & 0xE, g = pix >> 12 & 0xE, r = pix >> 4 & 0xE;
+#else
+			pixel_t r = pix >> 20 & 0xE, g = pix >> 12 & 0xE, b = pix >> 4 & 0xE;
+#endif
+			pix = b << 8 | g << 4 | r;
+			snprintf(buf, sizeof(buf), "Line: %d, Index: %d, Value: %03X", off_y- vdp->border_top, off_x >> 3, pix & 0xFFFFFF);
+			nk_layout_space_push(context, nk_rect(100, 512 - 32*5, windows[DEBUG_CRAM].tex_width, 32));
+			nk_label(context, buf, NK_TEXT_LEFT);
+		}
+		bounds.y += 512-32*4;
+		bounds.h = 32*4;
+		if (nk_input_is_mouse_hovering_rect(&context->input, bounds)) {
+			int index = ((((int)(context->input.mouse.pos.y - bounds.y)) >> 1) & 0xF0) + (((int)(context->input.mouse.pos.x - bounds.x)) >> 5);
+			snprintf(buf, sizeof(buf), "Index: %2d, Value: %03X", index, vdp->cram[index]);
+			nk_layout_space_push(context, nk_rect(100, 512 - 32*5, windows[DEBUG_CRAM].tex_width, 32));
+			nk_label(context, buf, NK_TEXT_LEFT);
+		}
+		
+		static struct nk_scroll scroll;
+		context->style.window.scrollbar_size.y = 0;
+		nk_layout_space_push(context, nk_rect(0, 0, 100, windows[DEBUG_CRAM].tex_height));
+		if (nk_group_scrolled_begin(context, &scroll, "Entries", 0)) {
+			nk_layout_space_begin(context, NK_STATIC, windows[DEBUG_CRAM].tex_height * 4, INT_MAX);
+			for (int i = 0; i < 64; i++)
+			{
+				nk_layout_space_push(context, nk_rect(0, i *32, 25, 32));
+				snprintf(buf, sizeof(buf), "%d", i);
+				nk_label(context, buf, NK_TEXT_RIGHT);
+				nk_layout_space_push(context, nk_rect(30, i *32, 50, 32));
+				snprintf(buf, sizeof(buf), "%03X", vdp->cram[i] & 0xEEE);
+				nk_edit_string_zero_terminated(context, NK_EDIT_FIELD, buf, sizeof(buf), nk_filter_hex);
+				char *end;
+				long newv = strtol(buf, &end, 16);
+				if (end != buf && newv != vdp->cram[i]) {
+					write_cram_internal(vdp, i, newv & 0xEEE);
+				}
+			}
+			nk_layout_space_end(context);
+			nk_group_scrolled_end(context);
+		}
+		
+		nk_end(context);
+	}
+	nk_sdl_render(context, NK_ANTI_ALIASING_ON, 512 * 1024, 128 * 1024);
+	
+	nk_input_begin(context);
+}
+#endif
+
+uint8_t debug_create_window(uint8_t debug_type, char *caption, uint32_t width, uint32_t height, window_close_handler close_handler)
+{
+#ifndef DISABLE_OPENGL
+	if (!render_has_gl()) {
+#endif
+		return render_create_window(caption, width, height, close_handler);
+#ifndef DISABLE_OPENGL
+	}
+	uint32_t win_width = width, win_height = height;
+	ui_render_fun render = NULL;
+	switch (debug_type)
+	{
+	case DEBUG_CRAM:
+		win_width += 100;
+		render = cram_debug_ui;
+		break;
+	}
+	if (render) {
+		//compensate for padding
+		win_width += 4 * 2;
+		win_height += 4 * 2;
+		windows[debug_type].win_idx = render_create_window_tex(caption, win_width, win_height, width, height, close_handler);
+		windows[debug_type].tex_width = width;
+		windows[debug_type].tex_height = width;
+		windows[debug_type].context = shared_nuklear_init(windows[debug_type].win_idx);
+		render_set_ui_render_fun(windows[debug_type].win_idx, render);
+		render_set_event_handler(windows[debug_type].win_idx, debug_handle_event);
+		return windows[debug_type].win_idx;
+	} else {
+		return render_create_window(caption, width, height, close_handler);
+	}
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuklear_ui/debug_ui.h	Thu Jun 19 19:59:05 2025 -0700
@@ -0,0 +1,8 @@
+#ifndef DEBUG_UI_H_
+#define DEBUG_UI_H_
+
+#include "../render.h"
+
+uint8_t debug_create_window(uint8_t debug_type, char *caption, uint32_t width, uint32_t height, window_close_handler close_handler);
+
+#endif //DEBUG_UI_H_
--- a/nuklear_ui/nuklear_sdl_gles2.h	Sun Jun 15 15:46:34 2025 -0700
+++ b/nuklear_ui/nuklear_sdl_gles2.h	Thu Jun 19 19:59:05 2025 -0700
@@ -29,13 +29,13 @@
 
 
 NK_API struct nk_context*   nk_sdl_init(SDL_Window *win);
-NK_API void                 nk_sdl_font_stash_begin(struct nk_font_atlas **atlas);
-NK_API void                 nk_sdl_font_stash_end(void);
-NK_API int                  nk_sdl_handle_event(SDL_Event *evt);
-NK_API void                 nk_sdl_render(enum nk_anti_aliasing , int max_vertex_buffer, int max_element_buffer);
-NK_API void                 nk_sdl_shutdown(void);
-NK_API void                 nk_sdl_device_destroy(void);
-NK_API void                 nk_sdl_device_create(void);
+NK_API void                 nk_sdl_font_stash_begin(struct nk_context *ctx, struct nk_font_atlas **atlas);
+NK_API void                 nk_sdl_font_stash_end(struct nk_context *ctx);
+NK_API int                  nk_sdl_handle_event(struct nk_context *ctx, SDL_Event *evt);
+NK_API void                 nk_sdl_render(struct nk_context *ctx, enum nk_anti_aliasing , int max_vertex_buffer, int max_element_buffer);
+NK_API void                 nk_sdl_shutdown(struct nk_context *ctx);
+NK_API void                 nk_sdl_device_destroy(struct nk_context *ctx);
+NK_API void                 nk_sdl_device_create(struct nk_context *ctx);
 
 #endif
 
@@ -75,14 +75,14 @@
 };
 #endif
 
-static struct nk_sdl {
+struct nk_sdl {
+    struct nk_context ctx;
     SDL_Window *win;
 #ifndef DISABLE_OPENGL
     struct nk_sdl_device ogl;
 #endif
-    struct nk_context ctx;
     struct nk_font_atlas atlas;
-} sdl;
+};
 
 #ifdef USE_GLES
 #define NK_SHADER_VERSION "#version 100\n"
@@ -94,9 +94,10 @@
 
 #ifndef DISABLE_OPENGL
 NK_API void
-nk_sdl_device_create(void)
+nk_sdl_device_create(struct nk_context *ctx)
 {
     GLint status;
+	struct nk_sdl *sdl = (struct nk_sdl *)ctx;
     static const GLchar *vertex_shader =
         NK_SHADER_VERSION
         "uniform mat4 ProjMtx;\n"
@@ -120,7 +121,7 @@
         "   gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV);\n"
         "}\n";
 
-    struct nk_sdl_device *dev = &sdl.ogl;
+    struct nk_sdl_device *dev = &sdl->ogl;
 
     nk_buffer_init_default(&dev->cmds);
     dev->prog = glCreateProgram();
@@ -162,9 +163,10 @@
 }
 
 NK_INTERN void
-nk_sdl_device_upload_atlas(const void *image, int width, int height)
+nk_sdl_device_upload_atlas(struct nk_context *ctx, const void *image, int width, int height)
 {
-    struct nk_sdl_device *dev = &sdl.ogl;
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
+    struct nk_sdl_device *dev = &sdl->ogl;
     glGenTextures(1, &dev->font_tex);
     glBindTexture(GL_TEXTURE_2D, dev->font_tex);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
@@ -178,9 +180,10 @@
 }
 
 NK_API void
-nk_sdl_device_destroy(void)
+nk_sdl_device_destroy(struct nk_context *ctx)
 {
-    struct nk_sdl_device *dev = &sdl.ogl;
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
+    struct nk_sdl_device *dev = &sdl->ogl;
     glDetachShader(dev->prog, dev->vert_shdr);
     glDetachShader(dev->prog, dev->frag_shdr);
     glDeleteShader(dev->vert_shdr);
@@ -193,9 +196,10 @@
 }
 
 NK_API void
-nk_sdl_render(enum nk_anti_aliasing AA, int max_vertex_buffer, int max_element_buffer)
+nk_sdl_render(struct nk_context *ctx, enum nk_anti_aliasing AA, int max_vertex_buffer, int max_element_buffer)
 {
-    struct nk_sdl_device *dev = &sdl.ogl;
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
+    struct nk_sdl_device *dev = &sdl->ogl;
     int display_width, display_height;
     struct nk_vec2 scale;
     GLfloat ortho[4][4] = {
@@ -204,7 +208,7 @@
         {0.0f, 0.0f,-1.0f, 0.0f},
         {-1.0f,1.0f, 0.0f, 1.0f},
     };
-    SDL_GL_GetDrawableSize(sdl.win, &display_width, &display_height);
+    SDL_GL_GetDrawableSize(sdl->win, &display_width, &display_height);
     ortho[0][0] /= (GLfloat)display_width;
     ortho[1][1] /= (GLfloat)display_height;
 
@@ -277,7 +281,7 @@
             /* setup buffers to load vertices and elements */
             nk_buffer_init_fixed(&vbuf, vertices, (nk_size)max_vertex_buffer);
             nk_buffer_init_fixed(&ebuf, elements, (nk_size)max_element_buffer);
-            nk_convert(&sdl.ctx, &dev->cmds, &vbuf, &ebuf, &config);
+            nk_convert(ctx, &dev->cmds, &vbuf, &ebuf, &config);
         }
         glBufferSubData(GL_ARRAY_BUFFER, 0, (size_t)max_vertex_buffer, vertices);
         glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, (size_t)max_element_buffer, elements);
@@ -285,7 +289,7 @@
         free(elements);
 
         /* iterate over and execute each draw command */
-        nk_draw_foreach(cmd, &sdl.ctx, &dev->cmds) {
+        nk_draw_foreach(cmd, ctx, &dev->cmds) {
             if (!cmd->elem_count) continue;
             glBindTexture(GL_TEXTURE_2D, (GLuint)cmd->texture.id);
             glScissor((GLint)(cmd->clip_rect.x * scale.x),
@@ -295,7 +299,7 @@
             glDrawElements(GL_TRIANGLES, (GLsizei)cmd->elem_count, GL_UNSIGNED_SHORT, offset);
             offset += cmd->elem_count;
         }
-        nk_clear(&sdl.ctx);
+        nk_clear(ctx);
         glDisableVertexAttribArray((GLuint)dev->attrib_pos);
         glDisableVertexAttribArray((GLuint)dev->attrib_uv);
         glDisableVertexAttribArray((GLuint)dev->attrib_col);
@@ -335,40 +339,43 @@
 NK_API struct nk_context*
 nk_sdl_init(SDL_Window *win)
 {
-    sdl.win = win;
-    nk_init_default(&sdl.ctx, 0);
-    sdl.ctx.clip.copy = nk_sdl_clipbard_copy;
-    sdl.ctx.clip.paste = nk_sdl_clipbard_paste;
-    sdl.ctx.clip.userdata = nk_handle_ptr(0);
-    return &sdl.ctx;
+    struct nk_sdl *sdl = calloc(1, sizeof(struct nk_sdl));
+    sdl->win = win;
+    nk_init_default(&sdl->ctx, 0);
+    sdl->ctx.clip.copy = nk_sdl_clipbard_copy;
+    sdl->ctx.clip.paste = nk_sdl_clipbard_paste;
+    sdl->ctx.clip.userdata = nk_handle_ptr(0);
+    return &sdl->ctx;
 }
 
 #ifndef DISABLE_OPENGL
 NK_API void
-nk_sdl_font_stash_begin(struct nk_font_atlas **atlas)
+nk_sdl_font_stash_begin(struct nk_context *ctx, struct nk_font_atlas **atlas)
 {
-    nk_font_atlas_init_default(&sdl.atlas);
-    nk_font_atlas_begin(&sdl.atlas);
-    *atlas = &sdl.atlas;
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
+    nk_font_atlas_init_default(&sdl->atlas);
+    nk_font_atlas_begin(&sdl->atlas);
+    *atlas = &sdl->atlas;
 }
 
 NK_API void
-nk_sdl_font_stash_end(void)
+nk_sdl_font_stash_end(struct nk_context *ctx)
 {
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
     const void *image; int w, h;
-    image = nk_font_atlas_bake(&sdl.atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
-    nk_sdl_device_upload_atlas(image, w, h);
-    nk_font_atlas_end(&sdl.atlas, nk_handle_id((int)sdl.ogl.font_tex), &sdl.ogl.null);
-    if (sdl.atlas.default_font)
-        nk_style_set_font(&sdl.ctx, &sdl.atlas.default_font->handle);
+    image = nk_font_atlas_bake(&sdl->atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
+    nk_sdl_device_upload_atlas(ctx, image, w, h);
+    nk_font_atlas_end(&sdl->atlas, nk_handle_id((int)sdl->ogl.font_tex), &sdl->ogl.null);
+    if (sdl->atlas.default_font)
+        nk_style_set_font(&sdl->ctx, &sdl->atlas.default_font->handle);
 
 }
 #endif
 
 NK_API int
-nk_sdl_handle_event(SDL_Event *evt)
+nk_sdl_handle_event(struct nk_context *ctx, SDL_Event *evt)
 {
-    struct nk_context *ctx = &sdl.ctx;
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
     if (evt->type == SDL_KEYUP || evt->type == SDL_KEYDOWN) {
         /* key events */
         int down = evt->type == SDL_KEYDOWN;
@@ -487,14 +494,15 @@
 }
 
 NK_API
-void nk_sdl_shutdown(void)
+void nk_sdl_shutdown(struct nk_context *ctx)
 {
-    nk_font_atlas_clear(&sdl.atlas);
-    nk_free(&sdl.ctx);
+    struct nk_sdl *sdl = (struct nk_sdl *)ctx;
+    nk_font_atlas_clear(&sdl->atlas);
+    nk_free(&sdl->ctx);
 #ifndef DISABLE_OPENGL
-    nk_sdl_device_destroy();
+    nk_sdl_device_destroy(ctx);
 #endif
-    memset(&sdl, 0, sizeof(sdl));
+    free(sdl);
 }
 
 #endif
--- a/render.h	Sun Jun 15 15:46:34 2025 -0700
+++ b/render.h	Thu Jun 19 19:59:05 2025 -0700
@@ -106,7 +106,11 @@
 uint8_t render_saving_video(void);
 void render_end_video(void);
 void render_save_video(char *path);
+uint8_t render_create_window_tex(char *caption, uint32_t width, uint32_t height, uint32_t tex_width, uint32_t tex_height, window_close_handler close_handler);
 uint8_t render_create_window(char *caption, uint32_t width, uint32_t height, window_close_handler close_handler);
+#ifndef DISABLE_OPENGL
+uint32_t render_get_window_texture(uint8_t which);
+#endif
 void render_destroy_window(uint8_t which);
 pixel_t *render_get_framebuffer(uint8_t which, int *pitch);
 void render_framebuffer_updated(uint8_t which, int width);
@@ -142,7 +146,7 @@
 uint8_t render_has_gl(void);
 void render_config_updated(void);
 void render_set_gl_context_handlers(ui_render_fun destroy, ui_render_fun create);
-void render_set_ui_render_fun(ui_render_fun);
+void render_set_ui_render_fun(uint8_t which, ui_render_fun);
 void render_set_ui_fb_resize_handler(ui_render_fun resize);
 void render_set_frame_presented_fun(ui_render_fun);
 void render_set_audio_full_fun(ui_render_fun);
--- a/render_sdl.c	Sun Jun 15 15:46:34 2025 -0700
+++ b/render_sdl.c	Thu Jun 19 19:59:05 2025 -0700
@@ -34,17 +34,21 @@
 
 
 typedef struct {
-	SDL_Window *win;
+	SDL_Window           *win;
 	SDL_Renderer         *renderer;
 	SDL_Texture          *sdl_texture;
 	SDL_Texture          **static_images;
 	window_close_handler on_close;
+	ui_render_fun        on_render;
+	event_handler        on_event;
 	uint32_t             width;
 	uint32_t             height;
 	uint8_t              num_static;
 #ifndef DISABLE_OPENGL
 	SDL_GLContext        *gl_context;
 	pixel_t              *texture_buf;
+	uint32_t             orig_tex_width;
+	uint32_t             orig_tex_height;
 	uint32_t             tex_width;
 	uint32_t             tex_height;
 	GLuint               gl_texture[2];
@@ -825,9 +829,13 @@
 }
 
 static event_handler custom_event_handler;
-void render_set_event_handler(event_handler handler)
+void render_set_event_handler(uint8_t which, event_handler handler)
 {
-	custom_event_handler = handler;
+	if (which < FRAMEBUFFER_USER_START) {
+		custom_event_handler = handler;
+	} else {
+		extras[which - FRAMEBUFFER_USER_START].on_event = handler;
+	}
 }
 
 int render_find_joystick_index(SDL_JoystickID instanceID)
@@ -952,26 +960,34 @@
 
 static int32_t handle_event(SDL_Event *event)
 {
-	if (custom_event_handler) {
-		custom_event_handler(event);
-	}
+	SDL_Window *event_win = NULL;
 	switch (event->type) {
 	case SDL_KEYDOWN:
-		handle_keydown(event->key.keysym.sym, scancode_map[event->key.keysym.scancode]);
+		event_win = SDL_GetWindowFromID(event->key.windowID);
+		if (event_win == main_window) {
+			handle_keydown(event->key.keysym.sym, scancode_map[event->key.keysym.scancode]);
+		}
 		break;
 	case SDL_KEYUP:
-		handle_keyup(event->key.keysym.sym, scancode_map[event->key.keysym.scancode]);
+		event_win = SDL_GetWindowFromID(event->key.windowID);
+		if (event_win == main_window) {
+			handle_keyup(event->key.keysym.sym, scancode_map[event->key.keysym.scancode]);
+		}
 		break;
 	case SDL_JOYBUTTONDOWN:
+		event_win = main_window;
 		handle_joydown(render_find_joystick_index(event->jbutton.which), event->jbutton.button);
 		break;
 	case SDL_JOYBUTTONUP:
+		event_win = main_window;
 		handle_joyup(lock_joystick_index(render_find_joystick_index(event->jbutton.which), -1), event->jbutton.button);
 		break;
 	case SDL_JOYHATMOTION:
+		event_win = main_window;
 		handle_joy_dpad(lock_joystick_index(render_find_joystick_index(event->jhat.which), -1), event->jhat.hat, event->jhat.value);
 		break;
 	case SDL_JOYAXISMOTION:
+		event_win = main_window;
 		handle_joy_axis(lock_joystick_index(render_find_joystick_index(event->jaxis.which), -1), event->jaxis.axis, event->jaxis.value);
 		break;
 	case SDL_JOYDEVICEADDED: {
@@ -1013,19 +1029,30 @@
 		break;
 	}
 	case SDL_MOUSEMOTION:
+		event_win = SDL_GetWindowFromID(event->motion.windowID);
 		handle_mouse_moved(event->motion.which, event->motion.x * ui_scale_x + 0.5f, event->motion.y * ui_scale_y + 0.5f + overscan_top[video_standard], event->motion.xrel, event->motion.yrel);
 		break;
 	case SDL_MOUSEBUTTONDOWN:
+		event_win = SDL_GetWindowFromID(event->button.windowID);
 		handle_mousedown(event->button.which, event->button.button);
 		break;
 	case SDL_MOUSEBUTTONUP:
+		event_win = SDL_GetWindowFromID(event->button.windowID);
 		handle_mouseup(event->button.which, event->button.button);
 		break;
+	case SDL_MOUSEWHEEL:
+		event_win = SDL_GetWindowFromID(event->wheel.windowID);
+		break;
+	case SDL_FINGERMOTION:
+	case SDL_FINGERDOWN:
+	case SDL_FINGERUP:
+		event_win = SDL_GetWindowFromID(event->tfinger.windowID);
+		break;
 	case SDL_WINDOWEVENT:
 		switch (event->window.event)
 		{
 		case SDL_WINDOWEVENT_SIZE_CHANGED:
-			if (!main_window) {
+			if (!main_window || SDL_GetWindowFromID(event->window.windowID) != main_window) {
 				break;
 			}
 			need_ui_fb_resize = 1;
@@ -1076,6 +1103,17 @@
 			break;
 		}
 		break;
+	case SDL_TEXTEDITING:
+		event_win = SDL_GetWindowFromID(event->edit.windowID);
+		break;
+#if SDL_VERSION_ATLEAST(2, 0, 22)
+	case SDL_TEXTEDITING_EXT:
+		event_win = SDL_GetWindowFromID(event->editExt.windowID);
+		break;
+#endif
+	case SDL_TEXTINPUT:
+		event_win = SDL_GetWindowFromID(event->text.windowID);
+		break;
 	case SDL_DROPFILE:
 		if (drag_drop_handler) {
 			drag_drop_handler(strdup(event->drop.file));
@@ -1086,6 +1124,23 @@
 		puts("");
 		exit(0);
 	}
+	if (event_win) {
+		if (event_win == main_window) {
+			if (custom_event_handler) {
+				custom_event_handler(FRAMEBUFFER_UI, event);
+			}
+		} else {
+			for (uint8_t i = 0; i < num_extras; i++)
+			{
+				if (extras[i].win == event_win) {
+					if (extras[i].on_event) {
+						extras[i].on_event(i + FRAMEBUFFER_USER_START, event);
+					}
+					break;
+				}
+			}
+		}
+	}
 	return 0;
 }
 
@@ -1546,14 +1601,15 @@
 	}
 }
 
-SDL_Window *render_get_window(void)
+SDL_Window *render_get_window(uint8_t which)
 {
+	SDL_Window *ret = which < FRAMEBUFFER_USER_START ? main_window : extras[which - FRAMEBUFFER_USER_START].win;
 #ifndef DISABLE_OPENGL
 	if (render_gl) {
-		SDL_GL_MakeCurrent(main_window, main_context);
+		SDL_GL_MakeCurrent(ret, which < FRAMEBUFFER_USER_START ? main_context : extras[which - FRAMEBUFFER_USER_START].gl_context);
 	}
 #endif
-	return main_window;
+	return ret;
 }
 
 uint32_t render_audio_syncs_per_sec(void)
@@ -1662,7 +1718,7 @@
 }
 #endif
 
-uint8_t render_create_window(char *caption, uint32_t width, uint32_t height, window_close_handler close_handler)
+uint8_t render_create_window_tex(char *caption, uint32_t width, uint32_t height, uint32_t tex_width, uint32_t tex_height, window_close_handler close_handler)
 {
 	uint8_t win_idx = 0xFF;
 	for (int i = 0; i < num_extras; i++)
@@ -1718,12 +1774,12 @@
 			if (i) {
 				glTexImage2D(GL_TEXTURE_2D, 0, INTERNAL_FORMAT, 1, 1, 0, SRC_FORMAT, SRC_TYPE, extras[win_idx].color);
 			} else {
-				extras[win_idx].tex_width = width;
-				extras[win_idx].tex_height = height;
+				extras[win_idx].tex_width = extras[win_idx].orig_tex_width = tex_width;
+				extras[win_idx].tex_height = extras[win_idx].orig_tex_height = tex_height;
 				char *npot_textures = tern_find_path_default(config, "video\0npot_textures\0", (tern_val){.ptrval = "off"}, TVAL_PTR).ptrval;
 				if (strcmp(npot_textures, "on")) {
-					extras[win_idx].tex_width = nearest_pow2(width);
-					extras[win_idx].tex_height = nearest_pow2(height);
+					extras[win_idx].tex_width = nearest_pow2(tex_width);
+					extras[win_idx].tex_height = nearest_pow2(tex_height);
 				}
 				extras[win_idx].texture_buf = calloc(PITCH_PIXEL_T(extras[win_idx].tex_width) * extras[win_idx].tex_height, sizeof(pixel_t));
 				glTexImage2D(GL_TEXTURE_2D, 0, INTERNAL_FORMAT, extras[win_idx].tex_width, extras[win_idx].tex_height, 0, SRC_FORMAT, SRC_TYPE, extras[win_idx].texture_buf);
@@ -1762,7 +1818,7 @@
 		if (!extras[win_idx].renderer) {
 			goto fail_renderer;
 		}
-		extras[win_idx].sdl_texture = SDL_CreateTexture(extras[win_idx].renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height);
+		extras[win_idx].sdl_texture = SDL_CreateTexture(extras[win_idx].renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, tex_width, tex_height);
 		if (!extras[win_idx].sdl_texture) {
 			goto fail_texture;
 		}
@@ -1780,6 +1836,18 @@
 	return 0;
 }
 
+uint8_t render_create_window(char *caption, uint32_t width, uint32_t height, window_close_handler close_handler)
+{
+	return render_create_window_tex(caption, width, height, width, height, close_handler);
+}
+
+#ifndef DISABLE_OPENGL
+uint32_t render_get_window_texture(uint8_t which)
+{
+	return extras[which - FRAMEBUFFER_USER_START].gl_texture[0];
+}
+#endif
+
 void render_destroy_window(uint8_t which)
 {
 	uint8_t win_idx = which - FRAMEBUFFER_USER_START;
@@ -2033,7 +2101,7 @@
 		return texture_buf;
 	} else if (render_gl && which >= FRAMEBUFFER_USER_START) {
 		uint8_t win_idx = which - FRAMEBUFFER_USER_START;
-		*pitch = PITCH_BYTES(extras[win_idx].width);
+		*pitch = PITCH_BYTES(extras[win_idx].tex_width);
 		return extras[win_idx].texture_buf;
 	} else {
 #endif
@@ -2163,7 +2231,7 @@
 		uint8_t win_idx = which - FRAMEBUFFER_USER_START;
 		SDL_GL_MakeCurrent(extras[win_idx].win, extras[win_idx].gl_context);
 		glBindTexture(GL_TEXTURE_2D, extras[win_idx].gl_texture[0]);
-		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, extras[win_idx].width, extras[win_idx].height, SRC_FORMAT, SRC_TYPE, buffer);
+		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, extras[win_idx].orig_tex_width, extras[win_idx].orig_tex_height, SRC_FORMAT, SRC_TYPE, buffer);
 	} else {
 #endif
 		uint32_t shot_height = height;
@@ -2230,14 +2298,17 @@
 		else {
 			glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
 			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-			
-			glBindBuffer(GL_ARRAY_BUFFER, extras[win_idx].gl_buffers[0]);
-			extra_draw_quad(
-				extras + win_idx,
-				extras[win_idx].gl_texture[0], 
-				(float)extras[win_idx].width / (float)extras[win_idx].tex_width,
-				(float)extras[win_idx].height / (float)extras[win_idx].tex_height
-			);
+			if (extras[win_idx].on_render) {
+				extras[win_idx].on_render();
+			} else {
+				glBindBuffer(GL_ARRAY_BUFFER, extras[win_idx].gl_buffers[0]);
+				extra_draw_quad(
+					extras + win_idx,
+					extras[win_idx].gl_texture[0], 
+					(float)extras[win_idx].orig_tex_width / (float)extras[win_idx].tex_width,
+					(float)extras[win_idx].orig_tex_height / (float)extras[win_idx].tex_height
+				);
+			}
 			
 			SDL_GL_SwapWindow(extras[win_idx].win);
 		}
@@ -2335,7 +2406,7 @@
 
 void render_framebuffer_updated(uint8_t which, int width)
 {
-	if (sync_src == SYNC_AUDIO_THREAD || sync_src == SYNC_EXTERNAL) {
+	if (which < FRAMEBUFFER_USER_START && (sync_src == SYNC_AUDIO_THREAD || sync_src == SYNC_EXTERNAL)) {
 		SDL_LockMutex(frame_mutex);
 			while (frame_queue_len == 4) {
 				SDL_CondSignal(frame_ready);
@@ -2417,9 +2488,13 @@
 }
 
 static ui_render_fun render_ui;
-void render_set_ui_render_fun(ui_render_fun fun)
+void render_set_ui_render_fun(uint8_t which, ui_render_fun fun)
 {
-	render_ui = fun;
+	if (which < FRAMEBUFFER_USER_START) {
+		render_ui = fun;
+	} else {
+		extras[which - FRAMEBUFFER_USER_START].on_render = fun;
+	}
 }
 
 static ui_render_fun frame_presented;
--- a/render_sdl.h	Sun Jun 15 15:46:34 2025 -0700
+++ b/render_sdl.h	Thu Jun 19 19:59:05 2025 -0700
@@ -3,9 +3,9 @@
 
 #include <SDL.h>
 
-SDL_Window *render_get_window(void);
-typedef void (*event_handler)(SDL_Event *);
-void render_set_event_handler(event_handler handler);
+SDL_Window *render_get_window(uint8_t which);
+typedef void (*event_handler)(uint8_t which, SDL_Event *);
+void render_set_event_handler(uint8_t which, event_handler handler);
 SDL_Joystick *render_get_joystick(int index);
 SDL_GameController *render_get_controller(int index);
 int render_find_joystick_index(SDL_JoystickID instanceID);
--- a/vdp.c	Sun Jun 15 15:46:34 2025 -0700
+++ b/vdp.c	Thu Jun 19 19:59:05 2025 -0700
@@ -11,6 +11,9 @@
 #include "util.h"
 #include "event_log.h"
 #include "terminal.h"
+#ifndef DISABLE_NUKLEAR
+#include "nuklear_ui/debug_ui.h"
+#endif
 
 #define NTSC_INACTIVE_START 224
 #define PAL_INACTIVE_START 240
@@ -6608,7 +6611,11 @@
 			return;
 		}
 		current_vdp = context;
+#ifdef DISABLE_NUKLEAR
 		context->debug_fb_indices[debug_type] = render_create_window(caption, width, height, vdp_debug_window_close);
+#else
+		context->debug_fb_indices[debug_type] = debug_create_window(debug_type, caption, width, height, vdp_debug_window_close);
+#endif
 		if (context->debug_fb_indices[debug_type]) {
 			context->enabled_debuggers |= 1 << debug_type;
 		}