essence-os/ports/uxn/emulator.c

365 lines
11 KiB
C

// Ported by nakst.
//
// Unfortunately, Uxn doesn't have a proper platform layer, so this port is a bit of a bodge.
// Perhaps once there is a more stable release of Uxn, someone can invest the time into making a proper port.
//
// TODO Keyboard support.
// TODO Audio support.
// TODO File support.
// TODO Time and date support.
#include <essence.h>
#define PPW (sizeof(unsigned int) * 2)
#define realloc EsCRTrealloc
#define memset EsCRTmemset
#define Uint32 uint32_t
typedef struct Ppu {
uint16_t width, height;
uint8_t *pixels, reqdraw;
} Ppu;
#ifdef DEBUG_BUILD
#include "../../bin/uxn/src/uxn.c"
#else
#include "../../bin/uxn/src/uxn-fast.c"
#endif
#include "../../bin/uxn/src/devices/ppu.c"
/*
Copyright (c) 2021 Devine Lu Linvega
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE.
*/
static Ppu ppu;
static Uxn u;
static Device *devsystem, *devscreen, *devmouse, *devctrl;
static Uint32 palette[16];
static unsigned int reqdraw = 0;
static int
clamp(int val, int min, int max)
{
return (val >= min) ? (val <= max) ? val : max : min;
}
static int
error(char *msg, const char *err)
{
EsPrint("%z: %z\n", msg, err);
return 0;
}
static void
domouse(int mx, int my, bool pressed, bool released, bool right)
{
Uint8 flag = 0x00;
Uint16 x = clamp(mx, 0, ppu.width - 1);
Uint16 y = clamp(my, 0, ppu.height - 1);
poke16(devmouse->dat, 0x2, x);
poke16(devmouse->dat, 0x4, y);
flag = right ? 0x10 : 0x01;
if (pressed) devmouse->dat[6] |= flag;
if (released) devmouse->dat[6] &= ~flag;
}
void
set_palette(Uint8 *addr)
{
int i;
for(i = 0; i < 4; ++i) {
Uint8
r = (*(addr + i / 2) >> (!(i % 2) << 2)) & 0x0f,
g = (*(addr + 2 + i / 2) >> (!(i % 2) << 2)) & 0x0f,
b = (*(addr + 4 + i / 2) >> (!(i % 2) << 2)) & 0x0f;
palette[i] = 0xff000000 | (r << 20) | (r << 16) | (g << 12) | (g << 8) | (b << 4) | b;
}
for(i = 4; i < 16; ++i)
palette[i] = palette[i / 4];
reqdraw = 1;
}
static Uint8
system_dei(Device *d, Uint8 port)
{
switch(port) {
case 0x2: return d->u->wst.ptr;
case 0x3: return d->u->rst.ptr;
default: return d->dat[port];
}
}
static void
system_deo(Device *d, Uint8 port)
{
switch(port) {
case 0x2: d->u->wst.ptr = d->dat[port]; break;
case 0x3: d->u->rst.ptr = d->dat[port]; break;
}
if(port > 0x7 && port < 0xe)
set_palette(&d->dat[0x8]);
}
static Uint8
screen_dei(Device *d, Uint8 port)
{
switch(port) {
case 0x2: return ppu.width >> 8;
case 0x3: return ppu.width;
case 0x4: return ppu.height >> 8;
case 0x5: return ppu.height;
default: return d->dat[port];
}
}
static void
screen_deo(Device *d, Uint8 port)
{
switch(port) {
case 0x1: d->vector = peek16(d->dat, 0x0); break;
case 0xe: {
Uint16 x = peek16(d->dat, 0x8);
Uint16 y = peek16(d->dat, 0xa);
Uint8 layer = d->dat[0xe] & 0x40;
ppu_write(&ppu, !!layer, x, y, d->dat[0xe] & 0x3);
if(d->dat[0x6] & 0x01) poke16(d->dat, 0x8, x + 1); /* auto x+1 */
if(d->dat[0x6] & 0x02) poke16(d->dat, 0xa, y + 1); /* auto y+1 */
break;
}
case 0xf: {
Uint16 x = peek16(d->dat, 0x8);
Uint16 y = peek16(d->dat, 0xa);
Uint8 layer = d->dat[0xf] & 0x40;
Uint8 *addr = &d->mem[peek16(d->dat, 0xc)];
if(d->dat[0xf] & 0x80) {
ppu_2bpp(&ppu, !!layer, x, y, addr, d->dat[0xf] & 0xf, d->dat[0xf] & 0x10, d->dat[0xf] & 0x20);
if(d->dat[0x6] & 0x04) poke16(d->dat, 0xc, peek16(d->dat, 0xc) + 16); /* auto addr+16 */
} else {
ppu_1bpp(&ppu, !!layer, x, y, addr, d->dat[0xf] & 0xf, d->dat[0xf] & 0x10, d->dat[0xf] & 0x20);
if(d->dat[0x6] & 0x04) poke16(d->dat, 0xc, peek16(d->dat, 0xc) + 8); /* auto addr+8 */
}
if(d->dat[0x6] & 0x01) poke16(d->dat, 0x8, x + 8); /* auto x+8 */
if(d->dat[0x6] & 0x02) poke16(d->dat, 0xa, y + 8); /* auto y+8 */
break;
}
}
}
static int
file_talk(Device *d, Uint8 b0, Uint8 w)
{
Uint8 read = b0 == 0xd;
if(w && (read || b0 == 0xf)) {
char *name = (char *)&d->mem[peek16(d->dat, 0x8)];
Uint16 result = 0, length = peek16(d->dat, 0xa);
long offset = (peek16(d->dat, 0x4) << 16) + peek16(d->dat, 0x6);
Uint16 addr = peek16(d->dat, b0 - 1);
size_t pathBytes;
char *path = EsStringAllocateAndFormat(&pathBytes, "|Settings:/%z", name);
EsFileInformation file = EsFileOpen(path, pathBytes, read ? (ES_FILE_READ | ES_NODE_FAIL_IF_NOT_FOUND) : (ES_FILE_WRITE));
EsHeapFree(path, 0, NULL);
if (file.error != ES_SUCCESS) {
result = 0;
} else if (read) {
result = EsFileReadSync(file.handle, offset, length, &d->mem[addr]);
EsHandleClose(file.handle);
} else {
if (!offset) EsFileResize(file.handle, length);
result = EsFileWriteSync(file.handle, offset, length, &d->mem[addr]);
EsHandleClose(file.handle);
}
poke16(d->dat, 0x2, result);
}
return 1;
}
uint16_t file_init(void *filename) { return 0; }
uint16_t file_read(void *dest, uint16_t len) { return 0; }
uint16_t file_write(void *dest, uint16_t len, uint8_t flags) { return 0; }
uint16_t file_stat(void *dest, uint16_t len) { return 0; }
uint16_t file_delete() { return 0; }
static void
file_deo(Device *d, Uint8 port)
{
switch(port) {
case 0x1: d->vector = peek16(d->dat, 0x0); break;
case 0x9: poke16(d->dat, 0x2, file_init(&d->mem[peek16(d->dat, 0x8)])); break;
case 0xd: poke16(d->dat, 0x2, file_read(&d->mem[peek16(d->dat, 0xc)], peek16(d->dat, 0xa))); break;
case 0xf: poke16(d->dat, 0x2, file_write(&d->mem[peek16(d->dat, 0xe)], peek16(d->dat, 0xa), d->dat[0x7])); break;
case 0x5: poke16(d->dat, 0x2, file_stat(&d->mem[peek16(d->dat, 0x4)], peek16(d->dat, 0xa))); break;
case 0x6: poke16(d->dat, 0x2, file_delete()); break;
}
}
static Uint8
nil_dei(Device *d, Uint8 port)
{
return d->dat[port];
}
static void
nil_deo(Device *d, Uint8 port)
{
if(port == 0x1) d->vector = peek16(d->dat, 0x0);
}
static const char *errors[] = {"underflow", "overflow", "division by zero"};
int
uxn_halt(Uxn *u, Uint8 error, char *name, int id)
{
EsPrint("Halted: %z %z#%x, at 0x%x\n", name, errors[error - 1], id, u->ram.ptr);
return 0;
}
bool Launch(const void *rom, size_t romBytes) {
if (romBytes > sizeof(u.ram.dat) - PAGE_PROGRAM) {
return false;
}
if (!uxn_boot(&u)) return false;
EsMemoryCopy(u.ram.dat + PAGE_PROGRAM, rom, romBytes);
reqdraw = 1;
const Uint16 width = 64 * 8, height = 40 * 8;
ppu_set_size(&ppu, width, height);
/* system */ devsystem = uxn_port(&u, 0x0, system_dei, system_deo);
/* console */ uxn_port(&u, 0x1, nil_dei, nil_deo);
/* screen */ devscreen = uxn_port(&u, 0x2, screen_dei, screen_deo);
/* audio0 */ uxn_port(&u, 0x3, nil_dei, nil_deo);
/* audio1 */ uxn_port(&u, 0x4, nil_dei, nil_deo);
/* audio2 */ uxn_port(&u, 0x5, nil_dei, nil_deo);
/* audio3 */ uxn_port(&u, 0x6, nil_dei, nil_deo);
/* unused */ uxn_port(&u, 0x7, nil_dei, nil_deo);
/* control */ devctrl = uxn_port(&u, 0x8, nil_dei, nil_deo);
/* mouse */ devmouse = uxn_port(&u, 0x9, nil_dei, nil_deo);
/* file */ uxn_port(&u, 0xa, nil_dei, file_deo);
/* datetime */ uxn_port(&u, 0xb, nil_dei, nil_deo);
/* unused */ uxn_port(&u, 0xc, nil_dei, nil_deo);
/* unused */ uxn_port(&u, 0xd, nil_dei, nil_deo);
/* unused */ uxn_port(&u, 0xe, nil_dei, nil_deo);
/* unused */ uxn_port(&u, 0xf, nil_dei, nil_deo);
uxn_eval(&u, PAGE_PROGRAM);
return true;
}
EsElement *canvas;
uint32_t imageWidth, imageHeight;
uint32_t *imageBits;
uint16_t timeDeltaMs;
bool inImageBounds;
EsRectangle GetImageBounds(EsRectangle bounds /* canvas bounds */) {
if (!imageWidth || !imageHeight) return ES_RECT_1(0);
int zoomX = ES_RECT_WIDTH(bounds) / imageWidth, zoomY = ES_RECT_HEIGHT(bounds) / imageHeight;
int zoom = zoomX < 1 || zoomY < 1 ? 1 : zoomX > zoomY ? zoomY : zoomX;
return EsRectangleCenter(bounds, ES_RECT_2S(imageWidth * zoom, imageHeight * zoom));
}
void MouseEvent(bool pressed, bool released, bool right) {
if (!imageWidth) return;
EsPoint point = EsMouseGetPosition(canvas);
EsRectangle imageBounds = GetImageBounds(EsElementGetInsetBounds(canvas));
inImageBounds = EsRectangleContains(imageBounds, point.x, point.y);
int zoom = ES_RECT_WIDTH(imageBounds) / imageWidth;
domouse((point.x - imageBounds.l) / zoom, (point.y - imageBounds.t) / zoom, pressed, released, right);
uxn_eval(&u, peek16(devmouse->dat, 0));
}
int CanvasMessage(EsElement *element, EsMessage *message) {
if (message->type == ES_MSG_PAINT) {
EsRectangle bounds = EsPainterBoundsInset(message->painter);
EsRectangle imageBounds = GetImageBounds(bounds);
EsDrawBitmapScaled(message->painter, imageBounds, ES_RECT_2S(imageWidth, imageHeight), imageBits, imageWidth * 4, ES_DRAW_BITMAP_OPAQUE);
EsDrawBlock(message->painter, ES_RECT_4(bounds.l, imageBounds.l, bounds.t, bounds.b), 0xFF000000);
EsDrawBlock(message->painter, ES_RECT_4(imageBounds.r, bounds.r, bounds.t, bounds.b), 0xFF000000);
EsDrawBlock(message->painter, ES_RECT_4(imageBounds.l, imageBounds.r, bounds.t, imageBounds.t), 0xFF000000);
EsDrawBlock(message->painter, ES_RECT_4(imageBounds.l, imageBounds.r, imageBounds.b, bounds.b), 0xFF000000);
} else if (message->type == ES_MSG_MOUSE_MOVED || message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_RIGHT_DRAG) {
MouseEvent(false, false, false);
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
MouseEvent(true, false, false);
} else if (message->type == ES_MSG_MOUSE_LEFT_UP) {
MouseEvent(false, true, false);
} else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) {
MouseEvent(true, false, true);
} else if (message->type == ES_MSG_MOUSE_RIGHT_UP) {
MouseEvent(false, true, false);
} else if (message->type == ES_MSG_GET_CURSOR && inImageBounds) {
message->cursorStyle = ES_CURSOR_BLANK;
} else if (message->type == ES_MSG_ANIMATE) {
message->animate.complete = false;
message->animate.waitMs = 16;
timeDeltaMs += message->animate.deltaMs;
bool needsRedraw = reqdraw;
while (timeDeltaMs > 16) {
uxn_eval(&u, peek16(devscreen->dat, 0));
if(devsystem->dat[0xe]) needsRedraw = true;
timeDeltaMs -= 16;
}
if (needsRedraw || ppu.reqdraw) {
if (imageWidth != ppu.width || imageHeight != ppu.height) {
imageWidth = ppu.width, imageHeight = ppu.height;
imageBits = (uint32_t *) EsHeapReallocate(imageBits, imageWidth * imageHeight * 4, false, NULL);
}
for (uint16_t y = 0; y < ppu.height; y++) {
for (uint16_t x = 0; x < ppu.width; x++) {
imageBits[x + y * ppu.width] = palette[ppu_read(&ppu, x, y)] | 0xFF000000;
}
}
EsRectangle imageBounds = GetImageBounds(EsElementGetInsetBounds(element));
EsElementRepaint(element, &imageBounds);
reqdraw = false;
ppu.reqdraw = false;
}
} else {
return 0;
}
return ES_HANDLED;
}
int InstanceCallback(EsInstance *instance, EsMessage *message) {
if (message->type == ES_MSG_INSTANCE_OPEN) {
size_t romBytes;
void *rom = EsFileStoreReadAll(message->instanceOpen.file, &romBytes);
EsInstanceOpenComplete(instance, message->instanceOpen.file, rom && Launch(rom, romBytes), NULL, 0);
EsHeapFree(rom, 0, NULL);
return ES_HANDLED;
}
return 0;
}
void _start() {
while (true) {
EsMessage *message = EsMessageReceive();
if (message->type == ES_MSG_INSTANCE_CREATE) {
EsInstance *instance = EsInstanceCreate(message, "Uxn Emulator", -1);
instance->callback = InstanceCallback;
canvas = EsCustomElementCreate(instance->window, ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER);
canvas->messageUser = (EsElementCallback) CanvasMessage;
EsElementStartAnimating(canvas);
}
}
}