#include <stdio.h>
#include <string.h>
#include <errno.h>
+#include <sysexits.h>
+#include <time.h>
#include <assert.h>
+#ifdef HAVE_LIBPNG
+#include <setjmp.h>
+#include <png.h>
+#endif /* HAVE_LIBPNG */
+
#include "dcpu16.h"
#include "display.h"
#include "chargen-4x8.h"
-/* preliminary attempt at handling display */
-/* currently keeps display state in buffer, and just updates a PNM file on each screen update */
+/*
+ preliminary attempt at handling display
+
+ currently keeps raw display output in a pixel buffer, which is updated on every write to cpu's memory in display-space.
+ every [n] cycles, display buffer is rendered into an image file (png or pnm) in memory
+
+ TODO: blinking
+*/
#define DISPLAY_BASE 0x8000
#define DISPLAY_END 0x8179
char b;
};
+/* buf will hold image file in memory */
+struct memstream_cookie_ {
+ char *buf;
+ size_t allocated;
+ size_t endpos;
+ off_t offset;
+ size_t chunk_size;
+};
+
struct dcpu16_display_ {
char *outfile;
- struct pixel_ *pixbuf;
+ struct pixel_ *pixbuf; /* raw pixels */
+ struct memstream_cookie_ memstream_cookie; /* streambuffer contains rendered image file from pixbuf */
+ unsigned long long cycle_last_write;
+ unsigned int pixbuf_dirty : 1;
+ unsigned int file_dirty : 1;
};
static inline
return p;
}
+/* support functions for writing streams into memory buffer */
+static
+void memstream_cookie_init_(struct memstream_cookie_ *c, size_t chunk_size) {
+ assert(c);
+ assert(chunk_size);
+
+ c->buf = NULL;
+ c->allocated = 0;
+ c->endpos = 0;
+ c->offset = 0;
+ c->chunk_size = chunk_size;
+}
+
+static
+void memstream_cookie_fini_(struct memstream_cookie_ *c) {
+ assert(c);
+
+ free(c->buf);
+ c->allocated = c->endpos = c->offset = 0;
+}
+
+static
+int memstream_write_fn_(void *c, const char *buf, int size) {
+ struct memstream_cookie_ *cookie = c;
+
+ while ((size_t)size + cookie->offset > cookie->allocated) {
+ size_t new_allocated = cookie->allocated + cookie->chunk_size;
+ void *tmp_ptr = realloc(cookie->buf, new_allocated);
+ if (tmp_ptr == NULL) {
+ return -1;
+ }
+ cookie->buf = tmp_ptr;
+ cookie->allocated = new_allocated;
+ }
+
+ memcpy(cookie->buf + cookie->offset, buf, size);
+ cookie->offset += size;
+ if (cookie->offset > (off_t)cookie->endpos) {
+ cookie->endpos = cookie->offset;
+ }
+
+ return size;
+}
+
/* should this just flood-fill entire display? */
static inline
-void display_draw_border(struct pixel_ *pixbuf, struct pixel_ color) {
+void display_draw_border_(struct pixel_ *pixbuf, struct pixel_ color) {
size_t x, y;
size_t i;
/* render a character cell to the display */
static inline
-void display_draw_cell(struct pixel_ *pixbuf, DCPU16_WORD *cell_map, DCPU16_WORD index, int cell_x, int cell_y, struct pixel_ fg, struct pixel_ bg) {
+void display_draw_cell_(struct pixel_ *pixbuf, DCPU16_WORD *cell_map, DCPU16_WORD index, int cell_x, int cell_y, struct pixel_ fg, struct pixel_ bg) {
struct pixel_ *cellstart = pixbuf; /* start of display */
unsigned int pix_x, pix_y;
unsigned char *cell_bitmap = (unsigned char *)(cell_map + (index * sizeof index));
}
}
-/* write pnm file */
-void display_pnm_write(struct pixel_ *pixbuf, const char *filename) {
+#ifdef HAVE_LIBPNG
+/* write status callback */
+static
+void display_png_write_row_cb_(png_structp png_ptr, png_uint_32 row, int pass) {
+ (void)png_ptr, (void)row, (void)pass;
+ /*
+ fprintf(stderr, "%s:%u:%d\n", __func__, row, pass);
+ */
+}
+
+static
+void display_png_user_error_fn_(png_structp png_ptr, png_const_charp error_msg) {
+ (void)png_ptr;
+ fprintf(stderr, "PNG:ERROR:%s\n", error_msg);
+ exit(EX_SOFTWARE);
+}
+
+static
+void display_png_user_warning_fn_(png_structp png_ptr, png_const_charp warning_msg) {
+ (void)png_ptr;
+ fprintf(stderr, "PNG:WARNING:%s\n", warning_msg);
+ return;
+}
+
+/* write png file */
+static
+void display_png_write_(struct dcpu16_display_ *d) {
+ FILE *f;
+ png_structp png_ptr;
+ png_infop info_ptr;
+ size_t i;
+
+ png_voidp user_error_ptr = NULL;
+
+#ifdef HAVE_FOPENCOOKIE
+ /* linux-style memory stream */
+ struct cookie_io_functions_t cookie_io_functions = {
+ .read = NULL,
+ .write = memstream_write_fn_,
+ .seek = NULL,
+ .close = NULL
+ };
+ f = fopencookie(&d->memstream_cookie, "wb", cookie_io_functions);
+ if (f == NULL) {
+ fprintf(stderr, "%s():%s\n", "fopencookie", strerror(errno));
+ return;
+ }
+
+#else /* HAVE_FOPENCOOKIE */
+#ifdef HAVE_FUNOPEN
+ /* BSD-style memory stream */
+ f = funopen(&d->memstream_cookie,
+ NULL, /* don't care about read */
+ memstream_write_fn_,
+ NULL, /* don't care about seek */
+ NULL /* don't care about close */
+ );
+ if (f == NULL) {
+ fprintf(stderr, "%s():%s\n", "funopen", strerror(errno));
+ return;
+ }
+
+#else /* HAVE_FUNOPEN */
+ /* default to writing file if we can't buffer in memory */
+ f = fopen(d->outfile, "wb");
+ if (f == NULL) {
+ return;
+ }
+#endif /* HAVE_FUNOPEN */
+#endif /* HAVE_FOPENCOOKIE */
+
+ png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, user_error_ptr, display_png_user_error_fn_, display_png_user_warning_fn_);
+ if (png_ptr == NULL) {
+ goto f_done;
+ }
+
+ info_ptr = png_create_info_struct(png_ptr);
+ if (info_ptr == NULL) {
+ png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
+ goto f_done;
+ }
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ goto f_done;
+}
+
+ png_init_io(png_ptr, f);
+ png_set_write_status_fn(png_ptr, display_png_write_row_cb_);
+
+ /* png_set_filter(png_ptr, 0, PNG_FILTER_NONE); */
+ /* png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); */
+
+ png_set_IHDR(png_ptr, info_ptr, PIX_X, PIX_Y, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+
+ png_write_info(png_ptr, info_ptr);
+
+ for (i = 0; i < PIX_Y; i++) {
+ png_write_row(png_ptr, (unsigned char *)(d->pixbuf + (i * PIX_X)));
+ }
+
+ png_write_end(png_ptr, info_ptr);
+
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+
+f_done:
+ fclose(f);
+}
+#else /* HAVE_LIBPNG */
+
+/* if PNG not available, just write pnm file */
+static
+void display_pnm_write_(struct dcpu16_display_ *d) {
size_t i;
FILE *f;
- f = fopen(filename, "w");
+ f = fopen(d->outfile, "w");
if (f == NULL) {
fprintf(stderr, "%s('%s'):%s\n", "fopen", filename, strerror(errno));
return;
/* write out image bytes in r g b order */
for (i = 0; i < PIX_X * PIX_Y; i++) {
- fwrite(&pixbuf[i].r, 1, 1, f);
- fwrite(&pixbuf[i].g, 1, 1, f);
- fwrite(&pixbuf[i].b, 1, 1, f);
+ fwrite(&d->pixbuf[i].r, 1, 1, f);
+ fwrite(&d->pixbuf[i].g, 1, 1, f);
+ fwrite(&d->pixbuf[i].b, 1, 1, f);
}
fclose(f);
}
+#endif /* HAVE_LIBPNG */
+
+static
+void display_image_render_(struct dcpu16_display_ *d) {
+#ifdef HAVE_LIBPNG
+ display_png_write_(d);
+#else /* HAVE_LIBPNG */
+ display_pnm_write_(d);
+#endif /* HAVE_LIBPNG */
+ d->file_dirty = 1;
+}
+
/* the callback to register to be run on display init/reset */
/* currently this populates the chargen map 'from rom'.. */
memset(vm->ram + DISPLAY_BASE, 0, (DISPLAY_END - DISPLAY_BASE) * sizeof *(vm->ram));
memset(d->pixbuf, 0, PIX_X * PIX_Y * sizeof *(d->pixbuf));
+
+ d->pixbuf_dirty = 1;
+}
+
+/* the callback to register with the cpu for occasionally doing something with the display buffer */
+void display_cycle_fn(struct dcpu16 *vm, dcpu16_acct_event e, DCPU16_WORD addr, void *data) {
+ struct dcpu16_display_ *d = (struct dcpu16_display_ *)data;
+
+ const unsigned long long min_cycles_between_writes = 1536;
+ const time_t min_seconds_between_file_write = 5;
+
+ static time_t last_file_write_ = 0;
+ time_t now;
+
+ (void)e, (void)addr;
+
+ if (d->pixbuf_dirty) {
+ if (vm->cycle > d->cycle_last_write + min_cycles_between_writes) {
+ display_image_render_(d);
+ d->pixbuf_dirty = 0;
+ d->cycle_last_write = vm->cycle;
+ }
+ }
+
+ if (d->file_dirty) {
+ now = time(NULL);
+ if (now > last_file_write_ + min_seconds_between_file_write) {
+ FILE *of;
+
+ of = fopen(d->outfile, "wb");
+ if (of == NULL) {
+ fprintf(stderr, "%s('%s'):%s\n", "fopen", d->outfile, strerror(errno));
+ return;
+ }
+
+ fwrite(d->memstream_cookie.buf, d->memstream_cookie.endpos, 1, of);
+ fclose(of);
+
+ d->file_dirty = 0;
+ last_file_write_ = now;
+ }
+ }
}
/* the callback to register with the cpu for watching memory updates */
-/* user data is an allocated display buffer */
+/* 'data' is an allocated display buffer */
void display_fn(struct dcpu16 *vm, dcpu16_acct_event e, DCPU16_WORD addr, void *data) {
DCPU16_DISPLAY *d = (DCPU16_DISPLAY *)data;
unsigned char index, blink, bg, fg;
blink = (vm->ram[addr] >> 7) & 0x01;
bg = (vm->ram[addr] >> 8) & 0x0f;
fg = (vm->ram[addr] >> 12) & 0x0f;
- display_draw_cell(d->pixbuf, vm->ram + DISPLAY_CELL_MAP, index, cell_x, cell_y, pcolor_(fg), pcolor_(bg));
+ display_draw_cell_(d->pixbuf, vm->ram + DISPLAY_CELL_MAP, index, cell_x, cell_y, pcolor_(fg), pcolor_(bg));
}
}
- display_pnm_write(d->pixbuf, d->outfile);
+ d->pixbuf_dirty = 1;
return;
}
if (addr == DISPLAY_MISC) {
/* new border color */
char border = vm->ram[addr] & 0x0f;
- display_draw_border(d->pixbuf, pcolor_(border));
+ display_draw_border_(d->pixbuf, pcolor_(border));
- display_pnm_write(d->pixbuf, d->outfile);
+ d->pixbuf_dirty = 1;
return;
}
bg = (vm->ram[addr] >> 8) & 0x0f;
fg = (vm->ram[addr] >> 12) & 0x0f;
- display_draw_cell(d->pixbuf, vm->ram + DISPLAY_CELL_MAP, index, cell_x, cell_y, pcolor_(fg), pcolor_(bg));
- display_pnm_write(d->pixbuf, d->outfile);
+ display_draw_cell_(d->pixbuf, vm->ram + DISPLAY_CELL_MAP, index, cell_x, cell_y, pcolor_(fg), pcolor_(bg));
+
+ d->pixbuf_dirty = 1;
}
/* init the pixel buffer */
return NULL;
}
+ memstream_cookie_init_(&d->memstream_cookie, 1024 * 8);
+
+ d->cycle_last_write = 0;
+ d->pixbuf_dirty = 0;
+ d->file_dirty = 0;
+
return d;
}
+
+void display_free(DCPU16_DISPLAY *d) {
+ if (d) {
+ free(d->pixbuf);
+ d->pixbuf = NULL;
+ free(d->outfile);
+ d->outfile = NULL;
+ memstream_cookie_fini_(&d->memstream_cookie);
+ }
+ free(d);
+}
"May be interrupted with SIGINT.\n");
}
+static const char * const display_filename_default_ =
+#ifdef HAVE_LIBPNG
+ "dcpu16-display.png"
+#else /* HAVE_LIBPNG */
+ "dcpu16-display.pnm"
+#endif /* HAVE_LIBPNG */
+;
COMMAND_IMPL(display) {
- (void)arg_count, (void)arg_vector;
-
static DCPU16_DISPLAY *display = NULL;
+ const char *filename = display_filename_default_;
+
+ if (arg_count == 2) {
+ filename = arg_vector[1];
+ }
if (display) {
printf("display already enabled..\n");
return 0;
}
- display = display_new("dcpu16-display.pnm");
+ display = display_new(filename);
+
if (display == NULL) {
fprintf(stderr, "failed to initialize display buffer\n");
return 0;
return 0;
}
+ if (dcpu16_acct_add(vm, DCPU16_ACCT_EV_CYCLE, display_cycle_fn, display)) {
+ fprintf(stderr, "failed to register display cycle callback\n");
+ return 0;
+ }
+
/* init display as if reset occurred */
display_reset_fn(vm, DCPU16_ACCT_EV_RESET, 0, display);
return 0;
}
COMMAND_HELP(display) {
- fprintf(f, "\tdisplay\n");
+ fprintf(f, "\tdisplay [file]\n");
if (summary) return;
- fprintf(f, "Begins updating a PNM of current display contents...\n"
- "This is not a fully-baked feature...\n");
+ fprintf(f, "Attaches display interface, begins updating an image file of display contents...\n"
+ "Image filename may be specified, defaults to '%s'\n",
+ display_filename_default_
+ );
}
/* gather all these together into a searchable table */
COMMAND_ENTRY(step, 0, 1),
COMMAND_ENTRY(run, 0, 0),
COMMAND_ENTRY(reset, 0, 0),
- COMMAND_ENTRY(display, 0, 0),
+ COMMAND_ENTRY(display, 0, 1),
{ NULL, 0, 0, NULL, NULL }
};