v1.7 spec mostly implemented, mostly untested
[dcpu16] / display.c
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <errno.h>
5 #include <sysexits.h>
6 #include <time.h>
7 #include <assert.h>
8
9 #ifdef HAVE_LIBPNG
10 #include <setjmp.h>
11 #include <png.h>
12 #endif /* HAVE_LIBPNG */
13
14 #include "dcpu16.h"
15 #include "display.h"
16 #include "chargen-4x8.h"
17
18 /*
19 preliminary attempt at handling display
20
21 currently keeps raw display output in a pixel buffer, which is updated on every write to cpu's memory in display-space.
22 every [n] cycles, display buffer is rendered into an image file (png or pnm) in memory
23
24 TODO: blinking
25 */
26
27 #define DISPLAY_BASE 0x8000
28 #define DISPLAY_END 0x8179
29 #define DISPLAY_CELL_MAP 0x8180
30 #define DISPLAY_MISC 0x8280
31
32 #define CELL_X 32
33 #define CELL_Y 12
34
35 #define CELL_X_SZ 4
36 #define CELL_Y_SZ 8
37
38 #define PIX_X 160
39 #define PIX_Y 128
40 #define PIX_BORDER 16
41
42 /* total display is 160 pixels by 128 pixels */
43 /* active display is 32x12 cells (each 4x8 pixels), surrounded by 16 pixel border */
44 /* cells are rendered from cell map, which is bitmap of two words, defining four eight-bit columns */
45
46 struct pixel_ {
47 unsigned char r;
48 unsigned char g;
49 unsigned char b;
50 };
51
52 /* buf will hold image file in memory */
53 struct memstream_cookie_ {
54 char *buf;
55 size_t allocated;
56 size_t endpos;
57 off_t offset;
58 size_t chunk_size;
59 };
60
61 struct dcpu16_display_ {
62 char *outfile;
63 struct pixel_ *pixbuf; /* raw pixels */
64 struct memstream_cookie_ memstream_cookie; /* streambuffer contains rendered image file from pixbuf */
65 unsigned long long cycle_last_write;
66 unsigned int pixbuf_dirty : 1;
67 unsigned int file_dirty : 1;
68 };
69
70 static inline
71 struct pixel_ pcolor_(unsigned int c) {
72 struct pixel_ p = { 0, 0, 0 };
73
74 switch (c) {
75 case 0x1: p.r=0x00, p.g=0x00, p.b=0xaa; break; /* dark blue */
76 case 0x2: p.r=0x00, p.g=0xaa, p.b=0x00; break; /* green */
77 case 0x3: p.r=0x00, p.g=0xaa, p.b=0xaa; break; /* cyan */
78 case 0x4: p.r=0xaa, p.g=0x00, p.b=0x00; break; /* red */
79 case 0x5: p.r=0xaa, p.g=0x00, p.b=0xaa; break; /* magenta */
80 case 0x6: p.r=0xaa, p.g=0xaa, p.b=0x55; break; /* yellow [] */
81 case 0x7: p.r=0xaa, p.g=0xaa, p.b=0xff; break; /* pale blue */
82 case 0x8: p.r=0x55, p.g=0x55, p.b=0x55; break; /* grey */
83 case 0x9: p.r=0x55, p.g=0x55, p.b=0xff; break; /* also blue */
84 case 0xa: p.r=0x55, p.g=0xff, p.b=0x55; break; /* light green */
85 case 0xb: p.r=0x55, p.g=0xff, p.b=0xff; break; /* light cyan */
86 case 0xc: p.r=0xff, p.g=0x55, p.b=0x55; break; /* light red */
87 case 0xd: p.r=0xff, p.g=0x55, p.b=0xff; break; /* light magenta */
88 case 0xe: p.r=0xff, p.g=0xff, p.b=0x55; break; /* light yellow */
89 case 0xf: p.r=0xff, p.g=0xff, p.b=0xff; break; /* white */
90 default: p.r=0x00, p.g=0x00, p.b=0x00; /* black */
91 }
92
93 return p;
94 }
95
96 /* support functions for writing streams into memory buffer */
97 static
98 void memstream_cookie_init_(struct memstream_cookie_ *c, size_t chunk_size) {
99 assert(c);
100 assert(chunk_size);
101
102 c->buf = NULL;
103 c->allocated = 0;
104 c->endpos = 0;
105 c->offset = 0;
106 c->chunk_size = chunk_size;
107 }
108
109 static
110 void memstream_cookie_fini_(struct memstream_cookie_ *c) {
111 assert(c);
112
113 free(c->buf);
114 c->allocated = c->endpos = c->offset = 0;
115 }
116
117 static
118 /* of course these can't be the same */
119 #ifdef HAVE_FOPENCOOKIE
120 ssize_t memstream_write_fn_(void *c, const char *buf, size_t size)
121 #else /* HAVE_FOPENCOOKIE */
122 int memstream_write_fn_(void *c, const char *buf, int size)
123 #endif /* HAVE_FOPENCOOKIE */
124 {
125 struct memstream_cookie_ *cookie = c;
126
127 while ((size_t)size + cookie->offset > cookie->allocated) {
128 size_t new_allocated = cookie->allocated + cookie->chunk_size;
129 void *tmp_ptr = realloc(cookie->buf, new_allocated);
130 if (tmp_ptr == NULL) {
131 return -1;
132 }
133 cookie->buf = tmp_ptr;
134 cookie->allocated = new_allocated;
135 }
136
137 memcpy(cookie->buf + cookie->offset, buf, size);
138 cookie->offset += size;
139 if (cookie->offset > (off_t)cookie->endpos) {
140 cookie->endpos = cookie->offset;
141 }
142
143 return size;
144 }
145
146 /* should this just flood-fill entire display? */
147 static inline
148 void display_draw_border_(struct pixel_ *pixbuf, struct pixel_ color) {
149 size_t x, y;
150 size_t i;
151
152 /* top */
153 for (y = 0; y < PIX_BORDER; y++) {
154 for (x = 0; x < PIX_X; x++) {
155 i = (y * PIX_X) + x;
156 pixbuf[i] = color;
157 i = ((PIX_Y - y) * PIX_X) + x;
158 pixbuf[i] = color;
159 }
160 }
161
162 /* sides */
163 for (y = PIX_BORDER; y < (PIX_Y - PIX_BORDER + 1); y++)
164 for (x = 0; x < PIX_BORDER; x++) {
165 i = (y * PIX_X) + x;
166 pixbuf[i] = color;
167 pixbuf[i + (PIX_X - PIX_BORDER)] = color;
168 }
169
170 }
171
172 /* render a character cell to the display */
173 static inline
174 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) {
175 struct pixel_ *cellstart = pixbuf; /* start of display */
176 unsigned int pix_x, pix_y;
177 unsigned char *cell_bitmap = (unsigned char *)(cell_map + (index * sizeof index));
178
179 assert(cell_x < CELL_X);
180 assert(cell_y < CELL_Y);
181
182 cellstart += (PIX_X * PIX_BORDER); /* skip top border */
183
184 cellstart += (CELL_Y_SZ * PIX_X) * cell_y; /* skip down to row */
185
186 cellstart += PIX_BORDER; /* skip side border */
187
188 cellstart += (CELL_X_SZ) * cell_x; /* skip to column */
189
190 for (pix_x = 0; pix_x < CELL_X_SZ; pix_x++) {
191 for (pix_y = 0; pix_y < CELL_Y_SZ; pix_y++) {
192 if ((cell_bitmap[pix_x] >> pix_y) & 0x01)
193 cellstart[((CELL_Y_SZ - pix_y - 1) * PIX_X) + pix_x] = fg;
194 else
195 cellstart[((CELL_Y_SZ - pix_y - 1) * PIX_X) + pix_x] = bg;
196 }
197 }
198 }
199
200 #ifdef HAVE_LIBPNG
201 /* write status callback */
202 static
203 void display_png_write_row_cb_(png_structp png_ptr, png_uint_32 row, int pass) {
204 (void)png_ptr, (void)row, (void)pass;
205 /*
206 fprintf(stderr, "%s:%u:%d\n", __func__, row, pass);
207 */
208 }
209
210 static
211 void display_png_user_error_fn_(png_structp png_ptr, png_const_charp error_msg) {
212 (void)png_ptr;
213 fprintf(stderr, "PNG:ERROR:%s\n", error_msg);
214 exit(EX_SOFTWARE);
215 }
216
217 static
218 void display_png_user_warning_fn_(png_structp png_ptr, png_const_charp warning_msg) {
219 (void)png_ptr;
220 fprintf(stderr, "PNG:WARNING:%s\n", warning_msg);
221 return;
222 }
223
224 /* write png file */
225 static
226 void display_png_write_(struct dcpu16_display_ *d) {
227 FILE *f;
228 png_structp png_ptr;
229 png_infop info_ptr;
230 size_t i;
231
232 png_voidp user_error_ptr = NULL;
233
234 #ifdef HAVE_FOPENCOOKIE
235 /* linux-style memory stream */
236 cookie_io_functions_t cookie_io_functions = {
237 .read = NULL,
238 .write = memstream_write_fn_,
239 .seek = NULL,
240 .close = NULL
241 };
242 f = fopencookie(&d->memstream_cookie, "wb", cookie_io_functions);
243 if (f == NULL) {
244 fprintf(stderr, "%s():%s\n", "fopencookie", strerror(errno));
245 return;
246 }
247
248 #else /* HAVE_FOPENCOOKIE */
249 #ifdef HAVE_FUNOPEN
250 /* BSD-style memory stream */
251 f = funopen(&d->memstream_cookie,
252 NULL, /* don't care about read */
253 memstream_write_fn_,
254 NULL, /* don't care about seek */
255 NULL /* don't care about close */
256 );
257 if (f == NULL) {
258 fprintf(stderr, "%s():%s\n", "funopen", strerror(errno));
259 return;
260 }
261
262 #else /* HAVE_FUNOPEN */
263 /* default to writing file if we can't buffer in memory */
264 f = fopen(d->outfile, "wb");
265 if (f == NULL) {
266 return;
267 }
268 #endif /* HAVE_FUNOPEN */
269 #endif /* HAVE_FOPENCOOKIE */
270
271 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, user_error_ptr, display_png_user_error_fn_, display_png_user_warning_fn_);
272 if (png_ptr == NULL) {
273 goto f_done;
274 }
275
276 info_ptr = png_create_info_struct(png_ptr);
277 if (info_ptr == NULL) {
278 png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
279 goto f_done;
280 }
281
282 if (setjmp(png_jmpbuf(png_ptr))) {
283 png_destroy_write_struct(&png_ptr, &info_ptr);
284 goto f_done;
285 }
286
287 png_init_io(png_ptr, f);
288 png_set_write_status_fn(png_ptr, display_png_write_row_cb_);
289
290 /* png_set_filter(png_ptr, 0, PNG_FILTER_NONE); */
291 /* png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); */
292
293 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);
294
295 png_write_info(png_ptr, info_ptr);
296
297 for (i = 0; i < PIX_Y; i++) {
298 png_write_row(png_ptr, (unsigned char *)(d->pixbuf + (i * PIX_X)));
299 }
300
301 png_write_end(png_ptr, info_ptr);
302
303 png_destroy_write_struct(&png_ptr, &info_ptr);
304
305 f_done:
306 fclose(f);
307 }
308 #else /* HAVE_LIBPNG */
309
310 /* if PNG not available, just write pnm file */
311 static
312 void display_pnm_write_(struct dcpu16_display_ *d) {
313 size_t i;
314 FILE *f;
315
316 f = fopen(d->outfile, "w");
317 if (f == NULL) {
318 fprintf(stderr, "%s('%s'):%s\n", "fopen", filename, strerror(errno));
319 return;
320 }
321
322 /* write header... */
323 /* PNM binary */
324 /* x y */
325 /* max value */
326 fprintf(f, "P6\n%d %d\n255\n", PIX_X, PIX_Y);
327
328 /* write out image bytes in r g b order */
329 for (i = 0; i < PIX_X * PIX_Y; i++) {
330 fwrite(&d->pixbuf[i].r, 1, 1, f);
331 fwrite(&d->pixbuf[i].g, 1, 1, f);
332 fwrite(&d->pixbuf[i].b, 1, 1, f);
333 }
334
335 fclose(f);
336 }
337
338 #endif /* HAVE_LIBPNG */
339
340 static
341 void display_image_render_(struct dcpu16_display_ *d) {
342 #ifdef HAVE_LIBPNG
343 display_png_write_(d);
344 #else /* HAVE_LIBPNG */
345 display_pnm_write_(d);
346 #endif /* HAVE_LIBPNG */
347 d->file_dirty = 1;
348 }
349
350
351 /* the callback to register to be run on display init/reset */
352 /* currently this populates the chargen map 'from rom'.. */
353 /* and clears the display buffers */
354 void display_reset_fn(struct dcpu16 *vm, dcpu16_acct_event e, DCPU16_WORD addr, void *data) {
355 struct dcpu16_display_ *d = (struct dcpu16_display_ *)data;
356
357 (void)e, (void)addr;
358
359 memcpy(vm->ram + DISPLAY_CELL_MAP, chargen_4x8_glyphs, sizeof chargen_4x8_glyphs);
360
361 memset(vm->ram + DISPLAY_BASE, 0, (DISPLAY_END - DISPLAY_BASE) * sizeof *(vm->ram));
362
363 memset(d->pixbuf, 0, PIX_X * PIX_Y * sizeof *(d->pixbuf));
364
365 d->pixbuf_dirty = 1;
366 }
367
368 /* the callback to register with the cpu for occasionally doing something with the display buffer */
369 void display_cycle_fn(struct dcpu16 *vm, dcpu16_acct_event e, DCPU16_WORD addr, void *data) {
370 struct dcpu16_display_ *d = (struct dcpu16_display_ *)data;
371
372 const unsigned long long min_cycles_between_writes = 1536;
373 const time_t min_seconds_between_file_write = 5;
374
375 static time_t last_file_write_ = 0;
376 time_t now;
377
378 (void)e, (void)addr;
379
380 if (d->pixbuf_dirty) {
381 if (vm->cycle > d->cycle_last_write + min_cycles_between_writes) {
382 display_image_render_(d);
383 d->pixbuf_dirty = 0;
384 d->cycle_last_write = vm->cycle;
385 }
386 }
387
388 if (d->file_dirty) {
389 now = time(NULL);
390 if (now > last_file_write_ + min_seconds_between_file_write) {
391 FILE *of;
392
393 of = fopen(d->outfile, "wb");
394 if (of == NULL) {
395 fprintf(stderr, "%s('%s'):%s\n", "fopen", d->outfile, strerror(errno));
396 return;
397 }
398
399 fwrite(d->memstream_cookie.buf, d->memstream_cookie.endpos, 1, of);
400 fclose(of);
401
402 d->file_dirty = 0;
403 last_file_write_ = now;
404 }
405 }
406 }
407
408 /* the callback to register with the cpu for watching memory updates */
409 /* 'data' is an allocated display buffer */
410 void display_fn(struct dcpu16 *vm, dcpu16_acct_event e, DCPU16_WORD addr, void *data) {
411 DCPU16_DISPLAY *d = (DCPU16_DISPLAY *)data;
412 unsigned char index, blink, bg, fg;
413 unsigned int cell_x, cell_y;
414
415 (void)e;
416
417 if (addr < DISPLAY_BASE || addr > DISPLAY_MISC)
418 return;
419
420 if (addr > DISPLAY_END && addr < DISPLAY_MISC) {
421 unsigned char updated_index = addr - DISPLAY_CELL_MAP;
422 /* a cell map was updated, refresh entire screen */
423 for (cell_x = 0; cell_x < CELL_X; cell_x++) {
424 for (cell_y = 0; cell_y < CELL_Y; cell_y++) {
425 addr = DISPLAY_BASE + cell_x + (cell_y * CELL_X);
426 index = vm->ram[addr] & 0x7f;
427 if (index != updated_index)
428 continue;
429 blink = (vm->ram[addr] >> 7) & 0x01;
430 bg = (vm->ram[addr] >> 8) & 0x0f;
431 fg = (vm->ram[addr] >> 12) & 0x0f;
432 display_draw_cell_(d->pixbuf, vm->ram + DISPLAY_CELL_MAP, index, cell_x, cell_y, pcolor_(fg), pcolor_(bg));
433 }
434 }
435
436 d->pixbuf_dirty = 1;
437 return;
438 }
439
440 if (addr == DISPLAY_MISC) {
441 /* new border color */
442 char border = vm->ram[addr] & 0x0f;
443 display_draw_border_(d->pixbuf, pcolor_(border));
444
445 d->pixbuf_dirty = 1;
446 return;
447 }
448
449 cell_x = (addr - DISPLAY_BASE) % CELL_X;
450 cell_y = (addr - DISPLAY_BASE) / CELL_X;
451
452 index = vm->ram[addr] & 0x7f;
453 blink = (vm->ram[addr] >> 7) & 0x01;
454 bg = (vm->ram[addr] >> 8) & 0x0f;
455 fg = (vm->ram[addr] >> 12) & 0x0f;
456
457 display_draw_cell_(d->pixbuf, vm->ram + DISPLAY_CELL_MAP, index, cell_x, cell_y, pcolor_(fg), pcolor_(bg));
458
459 d->pixbuf_dirty = 1;
460 }
461
462 /* init the pixel buffer */
463 DCPU16_DISPLAY *display_new(const char *filename) {
464 DCPU16_DISPLAY *d = calloc(1, sizeof *d);
465 if (d == NULL)
466 return NULL;
467
468 d->pixbuf = calloc(PIX_X * PIX_Y, sizeof *(d->pixbuf));
469 if (d->pixbuf == NULL) {
470 free(d);
471 return NULL;
472 }
473
474 d->outfile = strdup(filename);
475 if (d->outfile == NULL) {
476 free(d->pixbuf);
477 free(d);
478 return NULL;
479 }
480
481 memstream_cookie_init_(&d->memstream_cookie, 1024 * 8);
482
483 d->cycle_last_write = 0;
484 d->pixbuf_dirty = 0;
485 d->file_dirty = 0;
486
487 return d;
488 }
489
490 void display_free(DCPU16_DISPLAY *d) {
491 if (d) {
492 free(d->pixbuf);
493 d->pixbuf = NULL;
494 free(d->outfile);
495 d->outfile = NULL;
496 memstream_cookie_fini_(&d->memstream_cookie);
497 }
498 free(d);
499 }