/* dla2csv.c - convert dla.xex save files to CSV. Rather bloated and slow by Atari standards. */ #include #include #include #include #include #include #include #include #ifdef __CC65__ /* This will error out, if someone tries to compile for e.g. Apple or Commodore with cc65: */ #include #endif #include "dlaver.h" #define HEIGHT 170 #define WIDTH 176 #define BYTEWIDTH 22 /* cc65 doesn't fold constants, it won't let us do this: #define INBUF_SIZE (WIDTH * HEIGHT) ...it has to be a *constant*. */ #define INBUF_SIZE 3740 #define STRINGBUF_SIZE 256 void print_id(void) { printf("DLA to CSV converter v" VERSION ".\n"); } char inbuf[INBUF_SIZE]; char stringbuf[STRINGBUF_SIZE]; FILE *inf, *outf; /* We can't use string literal syntax, cc65 "helpfully" turns "\x0a" into an Atari 0x9b EOL character. Numeric constants are left alone. */ char a8eol[] = { 0x9b, 0x00, 0x00 }; char uxeol[] = { 0x0a, 0x00, 0x00 }; char mseol[] = { 0x0d, 0x0a, 0x00 }; char *eoltypes[][2] = { { "Atari ($9B)", a8eol }, { "Unix (\\n)", uxeol }, { "MS (\\r\\n)", mseol }, { NULL, NULL } }; char *eol; /* gets assigned one of the eoltypes */ /* using a table of masks instead of calculating them saves maybe 2s on the Atari. */ unsigned char masks[] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; #ifdef __ATARI__ /* Uncomment this to see elapsed time in convert() */ #define PROFILE /* I like these colors. Also they match dla.xex. */ #define TEXT_COLOR 0x0e #define TEXT_BG_COLOR 0x90 /* On the Atari, we should show Atari-style error messages, including the familiar error number. Another reason to do this: on the Atari, perror() says "Bad file number" instead of "No space left on device", if the disk fills up due to fprintf(). On modern platforms, just use perror(). */ #define PERROR(x) atari_perror(x) void atari_perror(char *msg) { printf("%s: Error %d: %s\n", msg, _oserror, _stroserror(_oserror)); } /* cc65 doesn't play nice with the Break key on the Atari. It works as expected during disk I/O, but if you press it while printing to stdout or reading from stdin, the program freezes. so call disable_break() at startup, then wrap disk I/O in enable_break() and disable_break(). */ void enable_break() { OS.pokmsk = POKEY_WRITE.irqen = (OS.pokmsk | 0x80); } void disable_break() { OS.pokmsk = POKEY_WRITE.irqen = (OS.pokmsk & 0x7f); } char old_color1, old_color2; void restore_colors(void) { OS.color1 = old_color1; OS.color2 = old_color2; } void init_console(void) { /* cc65's startup code turns off caps lock. turn it back on, since we're typing DOS filenames. in BASIC this would be: POKE 702,64 */ OS.shflok = 0x40; /* also, cc65 sets APPMHI to $bc1f (last byte before the GR.0 display list). which causes the atari to lock up when Reset is pressed. I can't believe this is useful behaviour... */ OS.appmhi = 0; /* clear the screen */ putchar(CH_CLR); /* save the old text & background colors */ old_color1 = OS.color1; old_color2 = OS.color2; /* Use my glorious and eye-catching color scheme :) */ OS.color1 = TEXT_COLOR; OS.color2 = TEXT_BG_COLOR; /* Put things back the way they were, when we exit. */ /* Nope. Using atexit() here makes MyDOS crash. Used atari_exit() and EXIT() macro instead. atexit(restore_colors); atexit(enable_break); */ } void print_banner(void) { unsigned char i; print_id(); printf("\n" "At any filename prompt, you may:\n" ); printf( "- Press Return, to exit this program.\n"); printf( "- Enter a number, to get a directory of\n" " that drive."); /* be nice to emulator users. */ for(i = 0; i < 12; i++) { if(OS.hatabs[i].id == 'H') { printf(" Use 0 for \"H:\"."); break; } } printf("\n"); } /* if the user enters a single digit, show directory of that drive. in a 3-column layout. this is a no-op on non-Atari. returns true if the user asked for a directory, false if not. */ char show_dir(void) { static char dirspec[16]; DIR *dir; struct dirent *ent; char device = 'D', drive = stringbuf[0]; unsigned char column = 0; if(!isdigit(drive)) return 0; if(drive == '0') { drive++; device = 'H'; } sprintf(dirspec, "%c%c:*.*", device, drive); if(!(dir = opendir(dirspec))) { dirspec[2] = '\0'; PERROR(dirspec); return 1; } errno = 0; while((ent = readdir(dir))) { printf("%-13s", ent->d_name); if(++column == 3) { column = 0; putchar('\n'); } } if(errno) PERROR(dirspec); closedir(dir); if(column) putchar('\n'); return 1; } void atari_exit(int status) { restore_colors(); enable_break(); exit(0); } #define EXIT(x) atari_exit(x) /* On the Atari, EOF is Ctrl-3. *Terrible* things happen if we ever get EOF on stdin on the Atari! The screen shows gibberish (display list gets mangled). Even calling exit() doesn't help. There's not really a way to avoid this (other than not using stdin, or hoping your users never press Ctrl-3), but we can avoid crashing the Atari by jumping directly to DOS (do not call the exit stuff from cc65's runtime, do not pass Go, do not collect $200). This seems to work reliably, but I hesistate to document "Press Ctrl-3 to exit"... ...and it turns out that on SpartaDOS X and other command-line DOSes, the display list doesn't get fixed: jmp (DOSVEC) just gives you the prompt, which expects the screen to still be usable... Sigh. Treat it as the Reset key instead. */ void exit_eof_stdin(void) { if(_is_cmdline_dos()) __asm__("jmp $e474"); /* cc65 doesn't define WARMSV for C */ else (*(OS.dosvec))(); } void print_converting(void) { printf("Press Ctrl-C to abort conversion.\n"); OS.crsinh = 1; printf("\nConverting...\n"); } #define enable_cursor() OS.crsinh = 0 char user_abort(void) { return OS.ch == (KEY_C | KEY_CTRL); } void clear_keystroke(void) { OS.ch = KEY_NONE; } #define REGISTER register /* using utoa() rather than fprintf() saves ~12 sec on the Atari. */ void writepoint(unsigned char x, unsigned char y) { static char ubuf[10]; static char fbuf[16]; utoa(x, fbuf, 10); strcat(fbuf, ","); strcat(fbuf, utoa(y, ubuf, 10)); strcat(fbuf, eol); fputs(fbuf, outf); } /* if user entered a filename with no device spec (D: or D1: etc), prepend D1:. this is a no-op on non-Atari. */ void fix_filename(void) { if(!strchr(stringbuf, ':')) { memmove(stringbuf+3, stringbuf, strlen(stringbuf) + 1); stringbuf[0] = 'D'; stringbuf[1] = '1'; stringbuf[2] = ':'; } } /* bar instead of percentage. j is chosen to range 1 to 80 (not 0 to 79), and this should only be called every other y loop. */ void update_progress(unsigned char i) { int j = (i * 200 / 212) + 1; if(j & 1) { putchar(0x19); /* left half block */ OS.colcrs--; } else { putchar(0xa0); /* full block */ } } #ifdef PROFILE #define start_profile_clock() OS.rtclok[0] = OS.rtclok[1] = OS.rtclok[2] = 0 #define print_profile_clock() \ printf("Elapsed time: %ds\n", ((OS.rtclok[1] << 8) | OS.rtclok[2]) / 60) #else #define start_profile_clock() #define print_profile_clock() #endif #else /* non-Atari is assumed to be POSIX (and not something like Commodore or Apple II). */ #define PERROR(x) perror(x) #define print_banner() print_id() #define noop() #define enable_break() noop() #define disable_break() noop() #define enable_cursor() noop() #define init_console() noop() #define clear_keystroke() noop() #define start_profile_clock() noop() #define print_profile_clock() noop() #define fix_filename() noop() #define update_progress(x) noop() #define user_abort() 0 #define show_dir() 0 #define EXIT(x) exit(1) #define exit_eof_stdin() exit(1) #define print_converting() printf("\nConverting...") #define REGISTER /* since utoa() only exists on cc65, can't use it on non-Atari. */ void writepoint(unsigned char x, unsigned char y) { fprintf(outf, "%d,%d%s", x, y, eol); } #endif /* Read a string from stdin (E: on the Atari). Exit if we get EOF on stdin (e.g. ^D on Linux, ^3 on Atari). See exit_eof_stdin() comments above. */ void readstring(void) { memset(stringbuf, 0, STRINGBUF_SIZE); if(fgets(stringbuf, STRINGBUF_SIZE - 1, stdin) == NULL) exit_eof_stdin(); } int prompt_yn(char *prompt, int default_y) { char *yn = "y/N"; if(default_y) yn = "Y/n"; printf("%s[%s]? ", prompt, yn); fflush(stdout); readstring(); switch(stringbuf[0]) { case 'y': case 'Y': return 1; break; case 'n': case 'N': return 0; break; default: break; } return default_y; } /* Prompt for a filename, try to open it. If there's an error, show error message and retry. Will not return until it opens the file. */ FILE *prompt_filename(const char *name, const char *mode) { FILE *f = NULL; putchar('\n'); while(f == NULL) { printf("%s file: ", name); fflush(stdout); readstring(); stringbuf[strlen(stringbuf) - 1] = '\0'; /* kill trailing \n */ if(strlen(stringbuf) == 0) { if(prompt_yn("Exit program", 0)) { EXIT(0); } continue; } if(show_dir()) continue; fix_filename(); enable_break(); f = fopen(stringbuf, mode); disable_break(); if(!f) PERROR(stringbuf); } return f; } /* Prompt for and read EOL type, retry if needed. Will not return until a valid number was entered. */ char *prompt_eol(void) { static int default_eoltype = 0; int i; putchar('\n'); for(i = 0; eoltypes[i][0] != NULL; i++) { printf("%d:%s ", i + 1, eoltypes[i][0]); } putchar('\n'); i = -1; while(i == -1) { printf("Line ending type[%d]? ", default_eoltype + 1); fflush(stdout); readstring(); if(stringbuf[0] == '\n') { i = default_eoltype; } else { i = stringbuf[0] - 49; /* ASCII 1-3 => 0-2 */ if(i < 0 || i > 2) i = -1; } } default_eoltype = i; return eoltypes[i][1]; } /* check_dla() returns 1 if all is well, 0 if there's a problem. */ int check_dla(int bytes) { int i, ok = 1; /* warn if the file size is wrong... */ if(bytes < INBUF_SIZE) { printf("Warning: File too short!\n"); ok = 0; } else if(fgetc(inf) != EOF) { printf("Warning: File too long!\n"); ok = 0; } else { printf("File size is OK.\n"); } fclose(inf); /* a DLA file never has non-zero pixels in the first 3 columns */ for(i = 0; i < INBUF_SIZE; i += (WIDTH / 8)) { if(inbuf[i] & 0xe0) { printf("Warning: File doesn't look like a DLA!\n"); ok = 0; break; } } return ok; } int read_file(void) { int bytes; /* in case of short read on 2nd or later conversion: */ memset(inbuf, 0, INBUF_SIZE); /* read whole input file into memory */ inf = prompt_filename("Input DLA", "rb"); printf("Reading..."); fflush(stdout); enable_break(); bytes = fread(inbuf, 1, INBUF_SIZE, inf); disable_break(); if(bytes <= 0) { PERROR(stringbuf); } return bytes; } /* currently, on the 800 with atariserver, a single-density disk image, and MyDOS (no high-speed SIO) convert() takes: - 15s for a DLA with 1000 points. - 8s for the same DLA, with the fputs() in writepoint() commented out. - 3s for a DLA with no points (a file of zeroes). writing the same amount of data as the CSV file (6577 bytes) with a single fwrite() takes 5.3s. comparing the first 2 times above, we're taking 7s for our I/O. buffering it up and doing a single write would save about 1.7s. however, for files with lots of points, we might run out of memory (so have to do multiple writes anyway). not worth doing really. the progress bar adds very little overhead (around 0.3 sec), compared to printing percentages (0.8 sec). what would be worth doing: speed up the pixel-extraction (the x loop and its inner j loop). it's doing ~200 particles/sec, not counting the 3s overhead (~125/sec with overhead). */ int convert(void) { unsigned char x, y, j, pixx; char byte; REGISTER char *inp = inbuf; int particles = 0; outf = prompt_filename("Output CSV", "wb"); print_converting(); /* CSV file header row (column names) */ fprintf(outf, "x,y%s", eol); /* write output file one line at a time */ /* loop over all the pixels, by row, then column. this loop is rather slow. */ for(y = 0; y < HEIGHT; ++y) { if(user_abort()) { printf("\nUser abort!\n"); fclose(outf); remove(stringbuf); clear_keystroke(); return 0; } if(y & 1) update_progress(y); for(x = 0; x < BYTEWIDTH; ++x) { if(*inp) { #if 1 byte = *inp; pixx = x * 8; for(j = 0; j < 8; ++j) { if(byte & masks[j]) { writepoint(pixx + j, y); /* commenting out the error-checking doesn't save time. */ if(ferror(outf)) { PERROR(stringbuf); fclose(outf); remove(stringbuf); return 0; } particles++; } } #else /* unrolling the loop doesn't save time (!) */ byte = *inp; pixx = x * 8; if(byte & masks[0]) writepoint(pixx, y); if(byte & masks[1]) writepoint(pixx + 1, y); if(byte & masks[2]) writepoint(pixx + 2, y); if(byte & masks[3]) writepoint(pixx + 3, y); if(byte & masks[4]) writepoint(pixx + 4, y); if(byte & masks[5]) writepoint(pixx + 5, y); if(byte & masks[6]) writepoint(pixx + 6, y); if(byte & masks[7]) writepoint(pixx + 7, y); #endif } inp++; } } fclose(outf); enable_cursor(); return particles; } int main(int argc, char **argv) { int done, result = 0; init_console(); print_banner(); while(1) { disable_break(); result = read_file(); if(result <= 0) continue; printf("Read %d bytes.\n", result); /* if anything looks wrong, we shouldn't proceed... but the user is boss, so give him the choice to shoot himself in the foot. */ if(!check_dla(result)) { if(!prompt_yn("Try to convert anyway", 0)) continue; } eol = prompt_eol(); /* convert in a loop. in case of write error, this allows retry without re-reading the input file. */ done = 0; while(!done) { start_profile_clock(); result = convert(); if(result >= 0) { done = 1; printf("%d particles.\n", result); print_profile_clock(); } else { if(!prompt_yn("Conversion failed, try again", 1)) done = 1; } } if(!prompt_yn("\nConvert another file", 1)) break; } EXIT(0); return 0; /* this never executes; it's here to shut up a warning */ }