shithub: psxe

Download patch

ref: a4fb5d06d406d9a21fcc767d81e870603d3b397a
parent: b958a4826fe639704e81351a844bb947c799839e
author: Jean-André Santoni <jean.andre.santoni@gmail.com>
date: Sat Mar 7 19:46:51 EST 2026

eui

--- /dev/null
+++ b/dat.h
@@ -1,0 +1,62 @@
+#ifndef DAT_H
+#define DAT_H
+
+#include "psx/p9.h"
+#include <draw.h>
+#include <thread.h>
+#include <keyboard.h>
+
+#include "eui.h"
+#include "psx/log.h"
+#include "psx/psx.h"
+#include "psx/dev/cdrom/cdrom.h"
+#include "psx/dev/gpu.h"
+#include "psx/dev/pad.h"
+#include "psx/dev/timer.h"
+#include "psx/input/sda.h"
+
+typedef struct Emu Emu;
+
+struct Emu {
+	psx_t *psx;
+	psx_pad_t *pad;
+	u64int prevkeys;
+};
+
+enum {
+	Vwdx = 640,
+	Vwdy = 480,
+};
+
+enum {
+	KeyCross = 1 << 0,
+	KeySquare = 1 << 1,
+	KeyTriangle = 1 << 2,
+	KeyCircle = 1 << 3,
+	KeyStart = 1 << 4,
+	KeySelect = 1 << 5,
+	KeyUp = 1 << 6,
+	KeyDown = 1 << 7,
+	KeyLeft = 1 << 8,
+	KeyRight = 1 << 9,
+	KeyL1 = 1 << 10,
+	KeyR1 = 1 << 11,
+	KeyL2 = 1 << 12,
+	KeyR2 = 1 << 13,
+	KeyL3 = 1 << 14,
+	KeyR3 = 1 << 15,
+	KeyAnalog = 1 << 16,
+};
+
+extern Emu emu;
+
+/* Frontend glue uses these directly. Keep explicit declarations local to psxe UI. */
+void psx_pad_button_press(psx_pad_t*, int, uint32_t);
+void psx_pad_button_release(psx_pad_t*, int, uint32_t);
+void psx_pad_attach_joy(psx_pad_t*, int, psx_input_t*);
+int psx_pad_attach_mcd(psx_pad_t*, int, const char*);
+psx_spu_t* psx_spu_create(void);
+void psx_spu_init(psx_spu_t*, psx_ic_t*);
+void psx_spu_destroy(psx_spu_t*);
+
+#endif
--- /dev/null
+++ b/eui.c
@@ -1,0 +1,415 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include "eui.h"
+
+typedef struct Kfn Kfn;
+
+u64int keys, keys2;
+int trace, paused;
+int savereq, loadreq;
+QLock pauselock;
+int scale, fixscale, warp10;
+uchar *pic;
+Rectangle picr;
+Mousectl *mc;
+Image *bg;
+
+static int profile, framestep;
+static int vwdx, vwdy, vwbpp;
+static ulong vwchan;
+static Image *fb;
+static Channel *conv, *sync[2];
+static uchar *screenconv[2], *backfb;
+static int screenconvi;
+
+struct Kfn{
+	Rune r;
+	int k;
+	char joyk[16];
+	void(*fn)(void);
+	Kfn *n;
+};
+static Kfn kfn, kkn;
+static int ax0, ax1;
+
+void *
+emalloc(ulong sz)
+{
+	void *v;
+
+	v = mallocz(sz, 1);
+	if(v == nil)
+		sysfatal("malloc: %r");
+	setmalloctag(v, getcallerpc(&sz));
+	return v;
+}
+
+Image *
+eallocimage(Rectangle r, ulong chan, int repl, ulong col)
+{
+	Image *i;
+
+	if((i = allocimage(display, r, chan, repl, col)) == nil)
+		sysfatal("allocimage: %r");
+	return i;
+}
+
+static void
+joyproc(void *)
+{
+	char buf[64], *s, *down[9];
+	int n, k, j;
+	Kfn *kp;
+
+	j = 1;
+
+	for(;;){
+		n = read(0, buf, sizeof(buf));
+		if(n <= 0)
+			sysfatal("read: %r");
+		buf[n-1] = 0;
+		n = getfields(buf, down, nelem(down), 0, " ");
+		k = 0;
+		for(n--; n >= 0; n--){
+			s = down[n];
+			if(strcmp(s, "joy1") == 0)
+				j = 1;
+			else if(strcmp(s, "joy2") == 0)
+				j = 2;
+			for(kp=kkn.n; kp!=nil; kp=kp->n){
+				if(strcmp(kp->joyk, s) == 0)
+					k |= kp->k;
+			}
+		}
+		if(j == 2)
+			keys2 = k;
+		else
+			keys = k;
+	}
+}
+
+static void
+keyproc(void *)
+{
+	int fd, n, k;
+	static char buf[256];
+	char *s;
+	Rune r;
+	Kfn *kp;
+
+	fd = open("/dev/kbd", OREAD);
+	if(fd < 0)
+		sysfatal("open: %r");
+	for(;;){
+		if(buf[0] != 0){
+			n = strlen(buf)+1;
+			memmove(buf, buf+n, sizeof(buf)-n);
+		}
+		if(buf[0] == 0){
+			n = read(fd, buf, sizeof(buf)-1);
+			if(n <= 0)
+				sysfatal("read /dev/kbd: %r");
+			buf[n-1] = 0;
+			buf[n] = 0;
+		}
+		if(buf[0] == 'c'){
+			if(utfrune(buf, Kdel)){
+				close(fd);
+				threadexitsall(nil);
+			}
+			if(utfrune(buf, KF|5))
+				savereq = 1;
+			if(utfrune(buf, KF|6))
+				loadreq = 1;
+			if(utfrune(buf, KF|12))
+				profile ^= 1;
+			if(utfrune(buf, 't'))
+				trace = !trace;
+			for(kp=kfn.n; kp!=nil; kp=kp->n){
+				if(utfrune(buf, kp->r))
+					kp->fn();
+			}
+		}
+		if(buf[0] != 'k' && buf[0] != 'K')
+			continue;
+		s = buf + 1;
+		k = 0;
+		while(*s != 0){
+			s += chartorune(&r, s);
+			switch(r){
+			case Kdel: close(fd); threadexitsall(nil);
+			case Kesc:
+				if(paused)
+					qunlock(&pauselock);
+				else
+					qlock(&pauselock);
+				paused = !paused;
+				break;
+			case KF|1:	
+				if(paused){
+					qunlock(&pauselock);
+					paused=0;
+				}
+				framestep = !framestep;
+				break;
+			case '`':
+				warp10 = !warp10;
+				break;
+			}
+			for(kp=kkn.n; kp!=nil; kp=kp->n)
+				if(kp->r == r){
+					k |= kp->k;
+					break;
+				}
+		}
+		if((k & ax0) == ax0)
+			k &= ~ax0;
+		if((k & ax1) == ax1)
+			k &= ~ax1;
+		keys = k;
+	}
+}
+
+static void
+timing(void)
+{
+	static int fcount;
+	static vlong old;
+	static char buf[32];
+	vlong new;
+
+	if(++fcount == 60)
+		fcount = 0;
+	else
+		return;
+	new = nsec();
+	if(new != old)
+		sprint(buf, "%6.2f%%", 1e11 / (new - old));
+	else
+		buf[0] = 0;
+	draw(screen, rectaddpt(Rect(10, 10, vwdx-40, 30), screen->r.min), bg, nil, ZP);
+	string(screen, addpt(screen->r.min, Pt(10, 10)), display->black, ZP, display->defaultfont, buf);
+	old = nsec();
+}
+
+static void
+screeninit(void)
+{
+	Point p;
+
+	send(sync[0], nil);
+	if(!fixscale){
+		scale = Dx(screen->r) / vwdx;
+		if(Dy(screen->r) / vwdy < scale)
+			scale = Dy(screen->r) / vwdy;
+	}
+	if(scale <= 0)
+		scale = 1;
+	else if(scale > 16)
+		scale = 16;
+	p = divpt(addpt(screen->r.min, screen->r.max), 2);
+	picr = Rpt(subpt(p, Pt(scale * vwdx/2, scale * vwdy/2)),
+		addpt(p, Pt(scale * vwdx/2, scale * vwdy/2)));
+	freeimage(fb);
+	fb = eallocimage(Rect(0, 0, scale * vwdx, scale > 1 ? 1 : vwdy),
+		vwchan, scale > 1, 0);
+	free(backfb);
+	if(scale > 1)
+		backfb = emalloc(vwdx * vwbpp * scale);
+	else
+		backfb = nil;
+	draw(screen, screen->r, bg, nil, ZP);
+	recv(sync[1], nil);
+}
+
+void
+flushmouse(int discard)
+{
+	Mouse m;
+
+	if(nbrecvul(mc->resizec) > 0){
+		send(sync[0], nil);
+		if(getwindow(display, Refnone) < 0)
+			sysfatal("resize failed: %r");
+		recv(sync[1], nil);
+		screeninit();
+	}
+	if(discard)
+		while(nbrecv(mc->c, &m) > 0)
+			;
+}
+
+static void
+screenproc(void*)
+{
+	uchar *p;
+	enum { Draw, Sync1, Sync2 };
+	Alt alts[] = {
+		[Draw]	{.c = conv, .v = &p, .op = CHANRCV},
+		[Sync1]	{.c = sync[0], .op = CHANRCV},
+		[Sync2]	{.c = sync[1], .op = CHANNOP},
+		{.op = CHANEND},
+	};
+
+	for(;;) switch(alt(alts)){
+	case Draw:
+		if(scale == 1){
+			loadimage(fb, fb->r, p, vwdx * vwdy * vwbpp);
+			draw(screen, picr, fb, nil, ZP);
+		} else {
+			Rectangle r;
+			int w, x;
+
+			r = picr;
+			w = vwdx * vwbpp * scale;
+			while(r.min.y < picr.max.y){
+				switch(vwbpp){
+				case 4: {
+					u32int *d = (u32int *)backfb, *e, s;
+					for(x=0; x<vwdx; x++){
+						s = *(u32int *)p;
+						p += vwbpp;
+						e = d + scale;
+						while(d < e)
+							*d++ = s;
+					}
+					break;
+				} case 2: {
+					u16int *d = (u16int *)backfb, *e, s;
+					for(x=0; x<vwdx; x++){
+						s = *(u16int *)p;
+						p += vwbpp;
+						e = d + scale;
+						while(d < e)
+							*d++ = s;
+					}
+					break;
+				} case 1: {
+					u8int *d = (u8int *)backfb, *e, s;
+					for(x=0; x<vwdx; x++){
+						s = *(u8int *)p;
+						p += vwbpp;
+						e = d + scale;
+						while(d < e)
+							*d++ = s;
+					}
+					break;
+				}}
+				loadimage(fb, fb->r, backfb, w);
+				r.max.y = r.min.y+scale;
+				draw(screen, r, fb, nil, ZP);
+				r.min.y = r.max.y;
+			}
+		}
+		flushimage(display, 1);
+		break;
+	case Sync1:
+		alts[Draw].op = CHANNOP;
+		alts[Sync1].op = CHANNOP;
+		alts[Sync2].op = CHANSND;
+		break;
+	case Sync2:
+		alts[Draw].op = CHANRCV;
+		alts[Sync1].op = CHANRCV;
+		alts[Sync2].op = CHANNOP;
+		break;
+	}
+}
+
+void
+flushscreen(void)
+{
+	memmove(screenconv[screenconvi], pic, vwdx * vwdy * vwbpp);
+	if(sendp(conv, screenconv[screenconvi]) > 0)
+		screenconvi = (screenconvi + 1) % 2;
+	if(profile)
+		timing();
+}
+
+void
+flushaudio(int (*audioout)(void))
+{
+	static vlong old, delta;
+	vlong new, diff;
+
+	if(audioout == nil || audioout() < 0 && !warp10){
+		new = nsec();
+		diff = 0;
+		if(old != 0){
+			diff = BILLION/60 - (new - old) - delta;
+			if(diff >= MILLION)
+				sleep(diff/MILLION);
+		}
+		old = nsec();
+		if(diff > 0){
+			diff = (old - new) - (diff / MILLION) * MILLION;
+			delta += (diff - delta) / 100;
+		}
+	}
+	if(framestep){
+		paused = 1;
+		qlock(&pauselock);
+		framestep = 0;
+	}
+}
+
+void
+regkeyfn(Rune r, void (*fn)(void))
+{
+	Kfn *kp;
+
+	for(kp=&kfn; kp->n!=nil; kp=kp->n)
+		;
+	kp->n = emalloc(sizeof *kp);
+	kp->n->r = r;
+	kp->n->fn = fn;
+}
+
+void
+regkey(char *joyk, Rune r, int k)
+{
+	Kfn *kp;
+
+	for(kp=&kkn; kp->n!=nil; kp=kp->n)
+		;
+	kp->n = emalloc(sizeof *kp);
+	strncpy(kp->n->joyk, joyk, sizeof(kp->n->joyk)-1);
+	if(strcmp(joyk, "up") == 0 || strcmp(joyk, "down") == 0)
+		ax0 |= k;
+	if(strcmp(joyk, "left") == 0 || strcmp(joyk, "right") == 0)
+		ax1 |= k;
+	kp->n->r = r;
+	kp->n->k = k;
+}
+
+void
+initemu(int dx, int dy, int bpp, ulong chan, int dokey, void(*kproc)(void*))
+{
+	vwdx = dx;
+	vwdy = dy;
+	vwchan = chan;
+	vwbpp = bpp;
+	if(initdraw(nil, nil, nil) < 0)
+		sysfatal("initdraw: %r");
+	mc = initmouse(nil, screen);
+	if(mc == nil)
+		sysfatal("initmouse: %r");
+	if(dokey)
+		proccreate(kproc != nil ? kproc : keyproc, nil, mainstacksize);
+	if(kproc == nil)
+		proccreate(joyproc, nil, mainstacksize);
+	bg = eallocimage(Rect(0, 0, 1, 1), screen->chan, 1, 0xCCCCCCFF);
+	scale = fixscale;
+	conv = chancreate(sizeof(uchar*), 0);
+	sync[0] = chancreate(1, 0);
+	sync[1] = chancreate(1, 0);
+	proccreate(screenproc, nil, mainstacksize);
+	pic = emalloc(vwdx * vwdy * vwbpp);
+	screenconv[0] = emalloc(vwdx * vwdy * vwbpp);
+	screenconv[1] = emalloc(vwdx * vwdy * vwbpp);
+	screeninit();
+}
--- /dev/null
+++ b/eui.h
@@ -1,0 +1,19 @@
+enum{
+	MILLION = 1000000,
+	BILLION = 1000000000,
+};
+
+extern u64int keys, keys2;
+extern int trace, paused;
+extern int savereq, loadreq;
+extern QLock pauselock;
+extern int scale, fixscale, warp10;
+extern uchar *pic;
+
+void*	emalloc(ulong);
+void	flushmouse(int);
+void	flushscreen(void);
+void	flushaudio(int(*)(void));
+void	regkeyfn(Rune, void(*)(void));
+void	regkey(char*, Rune, int);
+void	initemu(int, int, int, ulong, int, void(*)(void*));
--- /dev/null
+++ b/fns.h
@@ -1,0 +1,14 @@
+#ifndef FNS_H
+#define FNS_H
+
+typedef struct psx_gpu_t psx_gpu_t;
+
+int	audioout(void);
+void	bindkeys(void);
+void	blitframe(void);
+void	flush(void);
+void	process_inputs(void);
+void	psxe_gpu_vblank_event_cb(psx_gpu_t*);
+void	usage(void);
+
+#endif
--- a/mkfile
+++ b/mkfile
@@ -1,46 +1,16 @@
 </$objtype/mkfile
 
-# Core-only build for 9front/native ports.
-# Excludes frontend/ (SDL UI/audio loop) and builds reusable emulator core.
-LIB=libpsxe_core.a
+BIN=/$objtype/bin/games
+TARG=psxe
 CFLAGS=$CFLAGS -I. -Ipsx
 
 HFILES=\
-	psx/bus.h\
-	psx/bus_init.h\
-	psx/config.h\
-	psx/cpu.h\
-	psx/cpu_debug.h\
-	psx/exe.h\
-	psx/log.h\
-	psx/psx.h\
-	psx/input/guncon.h\
-	psx/input/sda.h\
-	psx/dev/bios.h\
-	psx/dev/dma.h\
-	psx/dev/exp1.h\
-	psx/dev/exp2.h\
-	psx/dev/gpu.h\
-	psx/dev/ic.h\
-	psx/dev/input.h\
-	psx/dev/mc1.h\
-	psx/dev/mc2.h\
-	psx/dev/mc3.h\
-	psx/dev/mcd.h\
-	psx/dev/mdec.h\
-	psx/dev/pad.h\
-	psx/dev/ram.h\
-	psx/dev/scratchpad.h\
-	psx/dev/spu.h\
-	psx/dev/timer.h\
-	psx/dev/xa.h\
-	psx/dev/cdrom/cdrom.h\
-	psx/dev/cdrom/cue.h\
-	psx/dev/cdrom/disc.h\
-	psx/dev/cdrom/list.h\
-	psx/dev/cdrom/queue.h\
+	dat.h\
+	fns.h\
 
 OFILES=\
+	psxe.$O\
+	eui.$O\
 	bus.$O\
 	config.$O\
 	cpu.$O\
@@ -75,15 +45,13 @@
 	list.$O\
 	queue.$O\
 
-default:V: $LIB
+</sys/src/cmd/mkone
 
 %.$O: psx/%.c
-	$CC $CFLAGS -o $target $prereq
+	$CC $CFLAGS psx/$stem.c
 %.$O: psx/input/%.c
-	$CC $CFLAGS -o $target $prereq
+	$CC $CFLAGS psx/input/$stem.c
 %.$O: psx/dev/%.c
-	$CC $CFLAGS -o $target $prereq
+	$CC $CFLAGS psx/dev/$stem.c
 %.$O: psx/dev/cdrom/%.c
-	$CC $CFLAGS -o $target $prereq
-
-</sys/src/cmd/mksyslib
+	$CC $CFLAGS psx/dev/cdrom/$stem.c
--- a/psx/bus.c
+++ b/psx/bus.c
@@ -14,58 +14,64 @@
 #include "dev/ic.h"
 #include "dev/scratchpad.h"
 #include "dev/gpu.h"
+#include "dev/spu.h"
+#include "dev/timer.h"
+#include "dev/pad.h"
+#include "dev/mdec.h"
 #include "log.h"
 
-/*
- * 6c struggles with the full SPU header in this TU; bus only needs
- * the MMIO prefix fields and read/write entry points.
- */
-struct psx_spu_t {
-    uint32_t bus_delay;
-    uint32_t io_base, io_size;
-};
-uint32_t psx_spu_read32(struct psx_spu_t*, uint32_t);
-uint16_t psx_spu_read16(struct psx_spu_t*, uint32_t);
-uint8_t psx_spu_read8(struct psx_spu_t*, uint32_t);
-void psx_spu_write32(struct psx_spu_t*, uint32_t, uint32_t);
-void psx_spu_write16(struct psx_spu_t*, uint32_t, uint16_t);
-void psx_spu_write8(struct psx_spu_t*, uint32_t, uint8_t);
+/* Keep explicit MMIO entrypoint declarations for 6c in this TU. */
+uint32_t psx_spu_read32(psx_spu_t*, uint32_t);
+uint16_t psx_spu_read16(psx_spu_t*, uint32_t);
+uint8_t psx_spu_read8(psx_spu_t*, uint32_t);
+void psx_spu_write32(psx_spu_t*, uint32_t, uint32_t);
+void psx_spu_write16(psx_spu_t*, uint32_t, uint16_t);
+void psx_spu_write8(psx_spu_t*, uint32_t, uint8_t);
+uint32_t psx_timer_read32(psx_timer_t*, uint32_t);
+uint16_t psx_timer_read16(psx_timer_t*, uint32_t);
+uint8_t psx_timer_read8(psx_timer_t*, uint32_t);
+void psx_timer_write32(psx_timer_t*, uint32_t, uint32_t);
+void psx_timer_write16(psx_timer_t*, uint32_t, uint16_t);
+void psx_timer_write8(psx_timer_t*, uint32_t, uint8_t);
+uint32_t psx_pad_read32(psx_pad_t*, uint32_t);
+uint16_t psx_pad_read16(psx_pad_t*, uint32_t);
+uint8_t psx_pad_read8(psx_pad_t*, uint32_t);
+void psx_pad_write32(psx_pad_t*, uint32_t, uint32_t);
+void psx_pad_write16(psx_pad_t*, uint32_t, uint16_t);
+void psx_pad_write8(psx_pad_t*, uint32_t, uint8_t);
+uint32_t psx_mdec_read32(psx_mdec_t*, uint32_t);
+uint16_t psx_mdec_read16(psx_mdec_t*, uint32_t);
+uint8_t psx_mdec_read8(psx_mdec_t*, uint32_t);
+void psx_mdec_write32(psx_mdec_t*, uint32_t, uint32_t);
+void psx_mdec_write16(psx_mdec_t*, uint32_t, uint16_t);
+void psx_mdec_write8(psx_mdec_t*, uint32_t, uint8_t);
+
+#define RANGE(v, s, e) ((v >= s) && (v < e))
 
-struct psx_timer_t {
+typedef struct psx_bus_iomap_t {
     uint32_t bus_delay;
     uint32_t io_base, io_size;
-};
-uint32_t psx_timer_read32(struct psx_timer_t*, uint32_t);
-uint16_t psx_timer_read16(struct psx_timer_t*, uint32_t);
-uint8_t psx_timer_read8(struct psx_timer_t*, uint32_t);
-void psx_timer_write32(struct psx_timer_t*, uint32_t, uint32_t);
-void psx_timer_write16(struct psx_timer_t*, uint32_t, uint16_t);
-void psx_timer_write8(struct psx_timer_t*, uint32_t, uint8_t);
+} psx_bus_iomap_t;
 
-struct psx_pad_t {
-    uint32_t bus_delay;
-    uint32_t io_base, io_size;
-};
-uint32_t psx_pad_read32(struct psx_pad_t*, uint32_t);
-uint16_t psx_pad_read16(struct psx_pad_t*, uint32_t);
-uint8_t psx_pad_read8(struct psx_pad_t*, uint32_t);
-void psx_pad_write32(struct psx_pad_t*, uint32_t, uint32_t);
-void psx_pad_write16(struct psx_pad_t*, uint32_t, uint16_t);
-void psx_pad_write8(struct psx_pad_t*, uint32_t, uint8_t);
+static int
+bus_probe(void *dev, uint32_t addr, uint32_t *off, uint32_t *cyc)
+{
+    psx_bus_iomap_t *h;
 
-struct psx_mdec_t {
-    uint32_t bus_delay;
-    uint32_t io_base, io_size;
-};
-uint32_t psx_mdec_read32(struct psx_mdec_t*, uint32_t);
-uint16_t psx_mdec_read16(struct psx_mdec_t*, uint32_t);
-uint8_t psx_mdec_read8(struct psx_mdec_t*, uint32_t);
-void psx_mdec_write32(struct psx_mdec_t*, uint32_t, uint32_t);
-void psx_mdec_write16(struct psx_mdec_t*, uint32_t, uint16_t);
-void psx_mdec_write8(struct psx_mdec_t*, uint32_t, uint8_t);
+    if (dev == nil)
+        return 0;
+
+    h = (psx_bus_iomap_t*)dev;
+
+    if (!RANGE(addr, h->io_base, (h->io_base + h->io_size)))
+        return 0;
+
+    *off = addr - h->io_base;
+    *cyc = h->bus_delay;
+
+    return 1;
+}
 
-#define RANGE(v, s, e) ((v >= s) && (v < e))
-
 const uint32_t g_psx_bus_region_mask_table[] = {
     0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
     0x7fffffff, 0x1fffffff, 0xffffffff, 0xffffffff
@@ -84,17 +90,21 @@
     free(bus);
 }
 
-#define HANDLE_READ_OP(dev, fn) \
-    if (RANGE(addr, bus->dev->io_base, (bus->dev->io_base + bus->dev->io_size))) { \
-        bus->access_cycles = bus->dev->bus_delay; \
-        return fn(bus->dev, addr - bus->dev->io_base); \
-    }
-#define HANDLE_WRITE_OP(dev, fn) \
-    if (RANGE(addr, bus->dev->io_base, (bus->dev->io_base + bus->dev->io_size))) { \
-        bus->access_cycles = bus->dev->bus_delay; \
-        fn(bus->dev, addr - bus->dev->io_base, value); \
+#define HANDLE_READ_OP(dev, fn) do { \
+    uint32_t off, cyc; \
+    if (bus_probe(bus->dev, addr, &off, &cyc)) { \
+        bus->access_cycles = cyc; \
+        return fn(bus->dev, off); \
+    } \
+} while (0)
+#define HANDLE_WRITE_OP(dev, fn) do { \
+    uint32_t off, cyc; \
+    if (bus_probe(bus->dev, addr, &off, &cyc)) { \
+        bus->access_cycles = cyc; \
+        fn(bus->dev, off, value); \
         return; \
-    }
+    } \
+} while (0)
 
 uint32_t psx_bus_read32(psx_bus_t* bus, uint32_t addr) {
     uint32_t vaddr = addr;
--- a/psx/bus.h
+++ b/psx/bus.h
@@ -1,11 +1,8 @@
-#ifndef BUS_H
-#define BUS_H
-
-#include "p9.h"
-
-struct psx_bus_t;
+#ifndef PSX_BUS_H
+#define PSX_BUS_H
 
-typedef struct psx_bus_t psx_bus_t;
+#include "p9.h"
+#include "bus_init.h"
 
 psx_bus_t* psx_bus_create(void);
 void psx_bus_init(psx_bus_t*);
--- a/psx/bus_init.h
+++ b/psx/bus_init.h
@@ -1,5 +1,5 @@
-#ifndef BUS_INIT_H
-#define BUS_INIT_H
+#ifndef PSX_BUS_INIT_H
+#define PSX_BUS_INIT_H
 
 #include "p9.h"
 
@@ -36,6 +36,8 @@
 typedef struct psx_cdrom_t psx_cdrom_t;
 typedef struct psx_pad_t psx_pad_t;
 typedef struct psx_mdec_t psx_mdec_t;
+
+typedef struct psx_bus_t psx_bus_t;
 
 struct psx_bus_t {
     struct psx_bios_t* bios;
--- a/psx/cpu.c
+++ b/psx/cpu.c
@@ -1,12 +1,12 @@
-#include "cpu.h"
-#include "bus.h"
-#include "log.h"
-
-#include "p9.h"
-
-#include "cpu_debug.h"
-
-static const uint32_t g_psx_cpu_cop0_write_mask_table[] = {
+#include "cpu.h"
+#include "bus.h"
+#include "log.h"
+
+#include "p9.h"
+
+#include "cpu_debug.h"
+
+static const uint32_t g_psx_cpu_cop0_write_mask_table[] = {
     0x00000000, // cop0r0   - N/A
     0x00000000, // cop0r1   - N/A
     0x00000000, // cop0r2   - N/A
--- a/psx/cpu.h
+++ b/psx/cpu.h
@@ -1,5 +1,5 @@
-#ifndef CPU_H
-#define CPU_H
+#ifndef PSX_CPU_H
+#define PSX_CPU_H
 
 #include "p9.h"
 
--- a/psx/exe.h
+++ b/psx/exe.h
@@ -1,5 +1,5 @@
-#ifndef EXE_H
-#define EXE_H
+#ifndef PSX_EXE_H
+#define PSX_EXE_H
 
 #include "p9.h"
 
@@ -48,4 +48,4 @@
 
 int psx_exe_load(psx_cpu_t*, const char*);
 
-#endif
\ No newline at end of file
+#endif
--- a/psx/psx.c
+++ b/psx/psx.c
@@ -1,4 +1,5 @@
 #include "psx.h"
+#include "dat.h"
 #include "dev/bios.h"
 #include "dev/ram.h"
 #include "dev/dma.h"
@@ -16,16 +17,7 @@
 #include "dev/pad.h"
 #include "dev/mdec.h"
 
-/* Keep explicit prototypes for 6c in this TU. */
-psx_bios_t* psx_bios_create(void);
-void psx_bios_init(psx_bios_t*);
-int psx_bios_load(psx_bios_t*, const char*);
-void psx_bios_destroy(psx_bios_t*);
-psx_spu_t* psx_spu_create(void);
-void psx_spu_init(psx_spu_t*, psx_ic_t*);
-void psx_spu_destroy(psx_spu_t*);
-
-psx_t* psx_create(void) {
+psx_t* psx_create(void) {
     return (psx_t*)malloc(sizeof(psx_t));
 }
 
--- a/psx/psx.h
+++ b/psx/psx.h
@@ -1,5 +1,5 @@
-#ifndef PSX_H
-#define PSX_H
+#ifndef PSX_PSX_H
+#define PSX_PSX_H
 
 #include "cpu.h"
 #include "log.h"
@@ -14,10 +14,10 @@
 #define PSXE_COMMIT STR(REP_COMMIT_HASH)
 #define PSXE_BUILD_OS STR(OS_INFO)
 
-typedef struct {
-    psx_bios_t* bios;
-    psx_ram_t* ram;
-    psx_dma_t* dma;
+typedef struct psx_t {
+    psx_bios_t* bios;
+    psx_ram_t* ram;
+    psx_dma_t* dma;
     psx_exp1_t* exp1;
     psx_exp2_t* exp2;
     psx_mc1_t* mc1;
@@ -30,10 +30,10 @@
     psx_bus_t* bus;
     psx_cpu_t* cpu;
     psx_timer_t* timer;
-    psx_cdrom_t* cdrom;
-    psx_pad_t* pad;
-    psx_mdec_t* mdec;
-} psx_t;
+    psx_cdrom_t* cdrom;
+    psx_pad_t* pad;
+    psx_mdec_t* mdec;
+} psx_t;
 
 psx_t* psx_create(void);
 int psx_init(psx_t*, const char*, const char*);
@@ -76,4 +76,4 @@
 psx_cpu_t* psx_get_cpu(psx_t*);
 void psx_destroy(psx_t*);
 
-#endif
\ No newline at end of file
+#endif
--- /dev/null
+++ b/psxe.c
@@ -1,0 +1,254 @@
+#include "dat.h"
+#include "fns.h"
+
+Emu emu;
+
+static u32int
+bgr555toxrgb32(u16int c)
+{
+	u32int r, g, b;
+
+	r = (c >> 0) & 0x1f;
+	g = (c >> 5) & 0x1f;
+	b = (c >> 10) & 0x1f;
+
+	r = (r << 3) | (r >> 2);
+	g = (g << 3) | (g >> 2);
+	b = (b << 3) | (b >> 2);
+
+	return 0xff000000 | (r << 16) | (g << 8) | b;
+}
+
+static void
+button_edge(u64int now, u64int bit, u32int mask)
+{
+	if(((now ^ emu.prevkeys) & bit) == 0)
+		return;
+
+	if((now & bit) != 0)
+		psx_pad_button_press(emu.pad, 0, mask);
+	else
+		psx_pad_button_release(emu.pad, 0, mask);
+}
+
+void
+process_inputs(void)
+{
+	u64int now;
+
+	now = keys;
+
+	button_edge(now, KeyCross, PSXI_SW_SDA_CROSS);
+	button_edge(now, KeySquare, PSXI_SW_SDA_SQUARE);
+	button_edge(now, KeyTriangle, PSXI_SW_SDA_TRIANGLE);
+	button_edge(now, KeyCircle, PSXI_SW_SDA_CIRCLE);
+	button_edge(now, KeyStart, PSXI_SW_SDA_START);
+	button_edge(now, KeySelect, PSXI_SW_SDA_SELECT);
+	button_edge(now, KeyUp, PSXI_SW_SDA_PAD_UP);
+	button_edge(now, KeyDown, PSXI_SW_SDA_PAD_DOWN);
+	button_edge(now, KeyLeft, PSXI_SW_SDA_PAD_LEFT);
+	button_edge(now, KeyRight, PSXI_SW_SDA_PAD_RIGHT);
+	button_edge(now, KeyL1, PSXI_SW_SDA_L1);
+	button_edge(now, KeyR1, PSXI_SW_SDA_R1);
+	button_edge(now, KeyL2, PSXI_SW_SDA_L2);
+	button_edge(now, KeyR2, PSXI_SW_SDA_R2);
+	button_edge(now, KeyL3, PSXI_SW_SDA_L3);
+	button_edge(now, KeyR3, PSXI_SW_SDA_R3);
+	button_edge(now, KeyAnalog, PSXI_SW_SDA_ANALOG);
+
+	emu.prevkeys = now;
+}
+
+void
+bindkeys(void)
+{
+	regkey("cross", 'x', KeyCross);
+	regkey("square", 'z', KeySquare);
+	regkey("triangle", 's', KeyTriangle);
+	regkey("circle", 'd', KeyCircle);
+	regkey("start", '\n', KeyStart);
+	regkey("select", Kshift, KeySelect);
+	regkey("up", Kup, KeyUp);
+	regkey("down", Kdown, KeyDown);
+	regkey("left", Kleft, KeyLeft);
+	regkey("right", Kright, KeyRight);
+	regkey("l1", 'q', KeyL1);
+	regkey("r1", 'w', KeyR1);
+	regkey("l2", '1', KeyL2);
+	regkey("r2", '3', KeyR2);
+	regkey("l3", 'a', KeyL3);
+	regkey("r3", 'f', KeyR3);
+	regkey("analog", '2', KeyAnalog);
+}
+
+void
+blitframe(void)
+{
+	u32int *dst, *drow;
+	u16int *s16, *row16;
+	uchar *s8, *row8;
+	void *src;
+	int fmt, sw, sh, dx, dy, x, y;
+
+	if(emu.psx == nil)
+		return;
+
+	src = psx_get_display_buffer(emu.psx);
+	fmt = psx_get_display_format(emu.psx);
+	sw = psx_get_display_width(emu.psx);
+	sh = psx_get_display_height(emu.psx);
+
+	if(emu.psx->gpu->disp_y + sh > PSX_GPU_FB_HEIGHT)
+		src = psx_get_vram(emu.psx);
+
+	if(sw <= 0 || sh <= 0)
+		return;
+
+	if(sw > Vwdx)
+		sw = Vwdx;
+	if(sh > Vwdy)
+		sh = Vwdy;
+
+	dx = (Vwdx - sw) / 2;
+	dy = (Vwdy - sh) / 2;
+
+	memset(pic, 0, Vwdx * Vwdy * 4);
+
+	dst = (u32int*)pic;
+
+	if(fmt != 0){
+		s8 = (uchar*)src;
+		for(y = 0; y < sh; y++){
+			row8 = s8 + (y * PSX_GPU_FB_STRIDE);
+			drow = dst + ((dy + y) * Vwdx) + dx;
+			for(x = 0; x < sw; x++){
+				u32int b, g, r;
+
+				b = row8[(x * 3) + 0];
+				g = row8[(x * 3) + 1];
+				r = row8[(x * 3) + 2];
+
+				drow[x] = 0xff000000 | (r << 16) | (g << 8) | b;
+			}
+		}
+		return;
+	}
+
+	s16 = (u16int*)src;
+	for(y = 0; y < sh; y++){
+		row16 = s16 + (y * PSX_GPU_FB_WIDTH);
+		drow = dst + ((dy + y) * Vwdx) + dx;
+		for(x = 0; x < sw; x++)
+			drow[x] = bgr555toxrgb32(row16[x]);
+	}
+}
+
+int
+audioout(void)
+{
+	return -1;
+}
+
+void
+flush(void)
+{
+	flushmouse(1);
+	flushscreen();
+	flushaudio(audioout);
+}
+
+void
+psxe_gpu_vblank_event_cb(psx_gpu_t *gpu)
+{
+	blitframe();
+	flush();
+	psxe_gpu_vblank_timer_event_cb(gpu);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-x scale] [-e expansion] bios [disc]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	char *bios, *disc, *exp;
+	psx_input_t *input;
+	psxi_sda_t *controller;
+	psx_gpu_t *gpu;
+	int r;
+
+	log_set_quiet(0);
+	log_set_level(LOG_WARN);
+
+	disc = nil;
+	exp = nil;
+
+	ARGBEGIN{
+	case 'x':
+		fixscale = strtol(EARGF(usage()), nil, 0);
+		break;
+	case 'e':
+		exp = EARGF(usage());
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc < 1 || argc > 2)
+		usage();
+
+	bios = argv[0];
+	if(argc == 2)
+		disc = argv[1];
+
+	memset(&emu, 0, sizeof emu);
+
+	emu.psx = psx_create();
+	if(emu.psx == nil)
+		sysfatal("psx_create: %r");
+
+	r = psx_init(emu.psx, bios, exp);
+	if(r != 0)
+		sysfatal("psx_init failed: %d", r);
+
+	emu.pad = psx_get_pad(emu.psx);
+
+	if(disc != nil && !psx_cdrom_open(psx_get_cdrom(emu.psx), disc))
+		fprint(2, "warning: cannot open disc %s\n", disc);
+
+	input = psx_input_create();
+	psx_input_init(input);
+
+	controller = psxi_sda_create();
+	psxi_sda_init(controller, SDA_MODEL_DIGITAL);
+	psxi_sda_init_input(controller, input);
+
+	psx_pad_attach_joy(emu.pad, 0, input);
+	psx_pad_attach_mcd(emu.pad, 0, "slot1.mcd");
+	psx_pad_attach_mcd(emu.pad, 1, "slot2.mcd");
+
+	gpu = psx_get_gpu(emu.psx);
+
+	psx_gpu_set_event_callback(gpu, GPU_EVENT_VBLANK, psxe_gpu_vblank_event_cb);
+	psx_gpu_set_event_callback(gpu, GPU_EVENT_HBLANK, psxe_gpu_hblank_event_cb);
+	psx_gpu_set_event_callback(gpu, GPU_EVENT_VBLANK_END, psxe_gpu_vblank_end_event_cb);
+	psx_gpu_set_event_callback(gpu, GPU_EVENT_HBLANK_END, psxe_gpu_hblank_end_event_cb);
+
+	psx_gpu_set_udata(gpu, 1, psx_get_timer(emu.psx));
+
+	initemu(Vwdx, Vwdy, 4, XRGB32, 1, nil);
+	bindkeys();
+
+	for(;;){
+		if(paused){
+			qlock(&pauselock);
+			qunlock(&pauselock);
+		}
+		process_inputs();
+		psx_update(emu.psx);
+	}
+}
--