/* vim:expandtab:ts=2 sw=2: */ /* Grafx2 - The Ultimate 256-color bitmap paint program Copyright 2018-2019 Thomas Bernard Copyright 2011 Pawel Góralski Copyright 2009 Petter Lindquist Copyright 2008 Yves Rizoud Copyright 2008 Franck Charlet Copyright 2007-2011 Adrien Destugues Copyright 1996-2001 Sunset Design (Guillaume Dorme & Karl Maritaud) Grafx2 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. Grafx2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Grafx2; if not, see */ ///@file motoformats.c /// Formats for the MO/TO Thomson machines #include #include #if defined(_MSC_VER) #define strdup _strdup #endif #include "struct.h" #include "io.h" #include "loadsave.h" #include "loadsavefuncs.h" #include "fileformats.h" #include "oldies.h" #include "input.h" #include "engine.h" #include "screen.h" #include "windows.h" #include "help.h" #include "gfx2mem.h" #include "gfx2log.h" extern char Program_version[]; // generated in pversion.c extern const char SVN_revision[]; // generated in version.c /////////////////////////////// Thomson Files /////////////////////////////// /** * Test for Thomson file */ void Test_MOTO(T_IO_Context * context, FILE * file) { long file_size; file_size = File_length_file(file); File_error = 1; if (file_size <= 10) return; switch (MOTO_Check_binary_file(file)) { case 0: // Not Thomson binary format switch (file_size) { // Files in RAW formats (from TGA2teo) case 8004: // 2 colors palette case 8008: // 4 colors palette case 8032: // 16 colors palette { char * filename; char * path; char * ext; // Check there are both FORME and COULEUR files filename = strdup(context->File_name); ext = strrchr(filename, '.'); if (ext == NULL || ext == filename) { free(filename); return; } if ((ext[-1] | 32) == 'c') ext[-1] = (ext[-1] & 32) | 'P'; else if ((ext[-1] | 32) == 'p') ext[-1] = (ext[-1] & 32) | 'C'; else { free(filename); return; } path = Filepath_append_to_dir(context->File_directory, filename); if (File_exists(path)) File_error = 0; free(path); free(filename); } return; default: break; } break; case 2: // MAP file (SAVEP/LOADP) case 3: // TO autoloading picture case 4: // MO autoloading picture File_error = 0; return; } } /** * Load a picture for Thomson TO8/TO8D/TO9/TO9+/MO6 * * One of the supported format is the one produced by TGA2Teo : * - Picture data is splitted into 2 files, one for each VRAM bank : * - The first VRAM bank is called "forme" (shape). * In 40col mode it stores pixels. * - The second VRAM bank is called "couleur" (color). * In 40col mode it store color indexes for foreground and background. * - File extension is .BIN, character before extension is "P" for the first * file, and "C" for the second. * - The color palette is stored in both files after the data. * * The mode is detected thanks to the number of color in the palette : * - 2 colors is 80col (640x200) * - 4 colors is bitmap4 (320x200 4 colors) * - 16 colors is either bitmap16 (160x200 16colors) * or 40col (320x200 16 colors with 2 unique colors in each 8x1 pixels * block). * * As it is not possible to disriminate bitmap16 and 40col, opening the "P" * file sets bitmap16, opening the "C" file sets 40col. * * This function also supports .MAP files (with optional TO-SNAP extension) * and our own "autoloading" BIN files. * See http://pulkomandy.tk/projects/GrafX2/wiki/Develop/FileFormats/MOTO for * a detailled description. */ void Load_MOTO(T_IO_Context * context) { // FORME / COULEUR FILE * file; byte * vram_forme = NULL; byte * vram_couleur = NULL; long file_size; int file_type; int bx, x, y, i; byte bpp = 4; byte code; word length, address; int transpose = 1; // transpose the upper bits of the color plane bytes // FFFFBBBB becomes bfFFFBBB (for TO7 compatibility) enum MOTO_Graphic_Mode mode = MOTO_MODE_40col; enum PIXEL_RATIO ratio = PIXEL_SIMPLE; int width = 320, height = 200, columns = 40; File_error = 1; file = Open_file_read(context); if (file == NULL) return; file_size = File_length_file(file); // Load default palette if (Config.Clear_palette) memset(context->Palette,0,sizeof(T_Palette)); MOTO_set_TO7_palette(context->Palette); file_type = MOTO_Check_binary_file(file); if (fseek(file, 0, SEEK_SET) < 0) { fclose(file); return; } if (file_type == 2) // MAP file { // http://collection.thomson.free.fr/code/articles/prehisto_bulletin/page.php?XI=0&XJ=13 byte map_mode, col_count, line_count; byte * vram_current; int end_marks; if (!(Read_byte(file,&code) && Read_word_be(file,&length) && Read_word_be(file,&address))) { fclose(file); return; } if (length < 5 || !(Read_byte(file,&map_mode) && Read_byte(file,&col_count) && Read_byte(file,&line_count))) { fclose(file); return; } length -= 3; columns = col_count + 1; height = 8 * (line_count + 1); switch(map_mode) { default: case 0: // bitmap4 or 40col width = 8 * columns; mode = MOTO_MODE_40col; // default to 40col bpp = 4; break; case 0x40: // bitmap16 columns >>= 1; width = 4 * columns; mode = MOTO_MODE_bm16; bpp = 4; ratio = PIXEL_WIDE; break; case 0x80: // 80col columns >>= 1; width = 16 * columns; mode = MOTO_MODE_80col; bpp = 1; ratio = PIXEL_TALL; break; } GFX2_Log(GFX2_DEBUG, "Map mode &H%02X row=%u line=%u (%dx%d) %d\n", map_mode, col_count, line_count, width, height, columns * height); vram_forme = GFX2_malloc(columns * height); vram_couleur = GFX2_malloc(columns * height); // Check extension (TO-SNAP / PPM / ???) if (length > 36) { long pos_backup; word data; pos_backup = ftell(file); fseek(file, length-2, SEEK_CUR); // go to last word of chunk Read_word_be(file, &data); GFX2_Log(GFX2_DEBUG, "%04X\n", data); switch (data) { case 0xA55A: // TO-SNAP fseek(file, -40, SEEK_CUR); // go to begin of extension Read_word_be(file, &data); // SCRMOD. 0=>40col, 1=>bm4, $40=>bm16, $80=>80col GFX2_Log(GFX2_DEBUG, "SCRMOD=&H%04X ", data); Read_word_be(file, &data); // Border color GFX2_Log(GFX2_DEBUG, "BORDER=%u ", data); Read_word_be(file, &data); // Mode BASIC (CONSOLE,,,,X) 0=40col, 1=80col, 2=bm4, 3=bm16, etc. GFX2_Log(GFX2_DEBUG, "CONSOLE,,,,%u\n", data); if(data == 2) { mode = MOTO_MODE_bm4; bpp = 2; } for (i = 0; i < 16; i++) { Read_word_be(file, &data); // Palette entry if (data & 0x8000) data = ~data; MOTO_gamma_correct_MOTO_to_RGB(&context->Palette[i], data); } snprintf(context->Comment, sizeof(context->Comment), "TO-SNAP .MAP file"); break; case 0x484C: // 'HL' PPM fseek(file, -36, SEEK_CUR); // go to begin of extension for (i = 0; i < 16; i++) { Read_word_be(file, &data); // Palette entry if (data & 0x8000) data = ~data; MOTO_gamma_correct_MOTO_to_RGB(&context->Palette[i], data); } Read_word_be(file, &data); // Mode BASIC (CONSOLE,,,,X) 0=40col, 1=80col, 2=bm4, 3=bm16, etc. GFX2_Log(GFX2_DEBUG, "CONSOLE,,,,%u\n", data); if(data == 2) { mode = MOTO_MODE_bm4; bpp = 2; } snprintf(context->Comment, sizeof(context->Comment), "PPM .MAP file"); break; default: snprintf(context->Comment, sizeof(context->Comment), "standard .MAP file"); } fseek(file, pos_backup, SEEK_SET); // RESET Position } i = 0; vram_current = vram_forme; end_marks = 0; while (length > 1) { byte byte1, byte2; Read_byte(file,&byte1); Read_byte(file,&byte2); length-=2; if(byte1 == 0) { if (byte2 == 0) { // end of vram stream GFX2_Log(GFX2_DEBUG, "0000 i=%d length=%ld\n", i, length); if (end_marks == 1) break; i = 0; vram_current = vram_couleur; end_marks++; } else while(byte2-- > 0 && length > 0) // copy { Read_byte(file,vram_current + i); length--; i += columns; // to the next line if (i >= columns * height) { if (mode == MOTO_MODE_bm4 || mode == MOTO_MODE_40col) i -= (columns * height - 1); // to the 1st line of the next column else { i -= columns * height; // back to the 1st line of the current column if (vram_current == vram_forme) // other VRAM vram_current = vram_couleur; else { vram_current = vram_forme; i++; // next column } } } } } else while(byte1-- > 0) // run length { vram_current[i] = byte2; i += columns; // to the next line if (i >= columns * height) { if (mode == MOTO_MODE_bm4 || mode == MOTO_MODE_40col) i -= (columns * height - 1); // to the 1st line of the next column else { i -= columns * height; // back to the 1st line of the current column if (vram_current == vram_forme) // other VRAM vram_current = vram_couleur; else { vram_current = vram_forme; i++; // next column } } } } } fclose(file); } else if(file_type == 3 || file_type == 4) { if (file_type == 4) // MO file { transpose = 0; MOTO_set_MO5_palette(context->Palette); } do { if (!(Read_byte(file,&code) && Read_word_be(file,&length) && Read_word_be(file,&address))) { if (vram_forme) break; fclose(file); return; } // MO5/MO6 VRAM address is &H0000 // TO7/TO8/TO9 VRAM addres is &H4000 if (length >= 8000 && length <= 8192 && (address == 0x4000 || address == 0)) { if (vram_forme == NULL) { vram_forme = calloc(8192, 1); Read_bytes(file, vram_forme, length); length = 0; } else if (vram_couleur == NULL) { vram_couleur = calloc(8192, 1); Read_bytes(file, vram_couleur, length); if (length >= 8032) { for (x = 0; x < 16; x++) { // 1 byte Blue (4 lower bits) // 1 byte Green (4 upper bits) / Red (4 lower bits) MOTO_gamma_correct_MOTO_to_RGB(&context->Palette[x], vram_couleur[8000+x*2]<<8 | vram_couleur[8000+x*2+1]); } if (length >= 8064) { memcpy(context->Comment, vram_couleur + 8032, 32); if (vram_couleur[8063] >= '0' && vram_couleur[8063] <= '3') mode = vram_couleur[8063] - '0'; } context->Comment[COMMENT_SIZE] = '\0'; } length = 0; } } if (length > 0) fseek(file, length, SEEK_CUR); } while(code == 0); fclose(file); switch (mode) { case MOTO_MODE_40col: // default break; case MOTO_MODE_bm4: bpp = 2; break; case MOTO_MODE_80col: bpp = 1; width = 640; ratio = PIXEL_TALL; break; case MOTO_MODE_bm16: width = 160; ratio = PIXEL_WIDE; break; } } else { char * filename; char * path; char * ext; int n_colors; vram_forme = GFX2_malloc(file_size); if (vram_forme == NULL) { fclose(file); return; } if (!Read_bytes(file, vram_forme, file_size)) { free(vram_forme); fclose(file); return; } n_colors = (file_size - 8000) / 2; switch(n_colors) { case 16: bpp = 4; // 16 colors : either 40col or bm16 mode ! // select later break; case 4: bpp = 2; mode = MOTO_MODE_bm4; break; default: bpp = 1; mode = MOTO_MODE_80col; width = 640; ratio = PIXEL_TALL; } filename = strdup(context->File_name); ext = strrchr(filename, '.'); if (ext == NULL || ext == filename) { free(vram_forme); free(filename); return; } if ((ext[-1] | 32) == 'c') { vram_couleur = vram_forme; vram_forme = NULL; ext[-1] = (ext[-1] & 32) | 'P'; } else if ((ext[-1] | 32) == 'p') { ext[-1] = (ext[-1] & 32) | 'C'; if (n_colors == 16) { mode = MOTO_MODE_bm16; width = 160; ratio = PIXEL_WIDE; } } else { free(vram_forme); free(filename); return; } path = Filepath_append_to_dir(context->File_directory, filename); file = fopen(path, "rb"); if (file == NULL) GFX2_Log(GFX2_ERROR, "Failed to open %s\n", path); free(path); free(filename); if (vram_forme == NULL) { vram_forme = GFX2_malloc(file_size); if (vram_forme == NULL) { free(vram_couleur); fclose(file); return; } Read_bytes(file,vram_forme,file_size); } else { vram_couleur = GFX2_malloc(file_size); if (vram_couleur == NULL) { free(vram_forme); fclose(file); return; } Read_bytes(file,vram_couleur,file_size); } fclose(file); GFX2_Log(GFX2_DEBUG, "MO/TO: %s,%s file_size=%ld n_colors=%d\n", context->File_name, filename, file_size, n_colors); for (x = 0; x < n_colors; x++) { // 1 byte Blue (4 lower bits) // 1 byte Green (4 upper bits) / Red (4 lower bits) MOTO_gamma_correct_MOTO_to_RGB(&context->Palette[x], vram_couleur[8000+x*2]<<8 | vram_couleur[8000+x*2+1]); } } Pre_load(context, width, height, file_size, FORMAT_MOTO, ratio, bpp); if (mode == MOTO_MODE_40col) Set_image_mode(context, IMAGE_MODE_THOMSON); File_error = 0; i = 0; for (y = 0; y < height; y++) { for (bx = 0; bx < columns; bx++) { byte couleur_forme; byte couleur_fond; byte forme, couleurs; forme = vram_forme[i]; if (vram_couleur) couleurs = vram_couleur[i]; else couleurs = (mode == MOTO_MODE_40col) ? 0x01 : 0x00; i++; switch(mode) { case MOTO_MODE_bm4: for (x = bx*8; x < bx*8+8; x++) { Set_pixel(context, x, y, ((forme & 0x80) >> 6) | ((couleurs & 0x80) >> 7)); forme <<= 1; couleurs <<= 1; } #if 0 // the following would be for the alternate bm4 mode for (x = bx*8; x < bx*8+4; x++) { Set_pixel(context, x, y, couleurs >> 6); couleurs <<= 2; } for (x = bx*8 + 4; x < bx*8+8; x++) { Set_pixel(context, x, y, forme >> 6); forme <<= 2; } #endif break; case MOTO_MODE_bm16: Set_pixel(context, bx*4, y, forme >> 4); Set_pixel(context, bx*4+1, y, forme & 0x0F); Set_pixel(context, bx*4+2, y, couleurs >> 4); Set_pixel(context, bx*4+3, y, couleurs & 0x0F); break; case MOTO_MODE_80col: for (x = bx*16; x < bx*16+8; x++) { Set_pixel(context, x, y, (forme & 0x80) >> 7); Set_pixel(context, x+8, y, (couleurs & 0x80) >> 7); forme <<= 1; couleurs <<= 1; } break; case MOTO_MODE_40col: default: if (transpose) { // the color plane byte is bfFFFBBB (for TO7 compatibility) // with the upper bits of both foreground (forme) and // background (fond) inverted. couleur_forme = ((couleurs & 0x78) >> 3) ^ 0x08; couleur_fond = ((couleurs & 7) | ((couleurs & 0x80) >> 4)) ^ 0x08; } else { // MO5 : the color plane byte is FFFFBBBB couleur_forme = couleurs >> 4; couleur_fond = couleurs & 0x0F; } for (x = bx*8; x < bx*8+8; x++) { Set_pixel(context, x, y, (forme & 0x80)?couleur_forme:couleur_fond); forme <<= 1; } } } } free(vram_forme); free(vram_couleur); } /** * Pack a stream of byte in the format used by Thomson MO/TO MAP files. * * - 00 cc xx yy .. : encodes a "copy run" (cc = bytes to copy) * - cc xx : encodes a "repeat run" (cc > 0 : count) */ //#define MOTO_MAP_NOPACKING unsigned int MOTO_MAP_pack(byte * packed, const byte * unpacked, unsigned int unpacked_len) { unsigned int src; unsigned int dst = 0; unsigned int count; #ifndef MOTO_MAP_NOPACKING unsigned int repeat; unsigned int i; word * counts; #endif GFX2_Log(GFX2_DEBUG, "MOTO_MAP_pack(%p, %p, %u)\n", packed, unpacked, unpacked_len); if (unpacked_len == 0) return 0; if (unpacked_len == 1) { packed[0] = 1; packed[1] = unpacked[0]; return 2; } #ifdef MOTO_MAP_NOPACKING // compression disabled src = 0; while ((unpacked_len - src) > 255) { packed[dst++] = 0; packed[dst++] = 255; memcpy(packed+dst, unpacked+src, 255); dst += 255; src += 255; } count = unpacked_len - src; packed[dst++] = 0; packed[dst++] = count; memcpy(packed+dst, unpacked+src, count); dst += count; src += count; return dst; #else counts = GFX2_malloc(sizeof(word) * (unpacked_len + 1)); i = 0; repeat = (unpacked[0] == unpacked[1]); count = 2; src = 2; // 1st step : count lenght of the Copy runs and Repeat runs while (src < unpacked_len) { if (repeat) { if (unpacked[src-1] == unpacked[src]) count++; else { // flush the repeat run counts[i++] = count | 0x8000; // 0x8000 is the marker for repeat runs count = 1; repeat = 0; } } else { if (unpacked[src-1] != unpacked[src]) count++; else if (count == 1) { count++; repeat = 1; } else { // flush the copy run counts[i++] = (count-1) | (count == 2 ? 0x8000 : 0); // mark copy run of 1 as repeat of 1 count = 2; repeat = 1; } } src++; } // flush the last run counts[i++] = ((repeat || count == 1) ? 0x8000 : 0) | count; counts[i++] = 0; // end marker // check consistency of counts count = 0; for (i = 0; counts[i] != 0; i++) count += (counts[i] & ~0x8000); if (count != unpacked_len) GFX2_Log(GFX2_ERROR, "*** encoding error in MOTO_MAP_pack() *** count=%u unpacked_len=%u\n", count, unpacked_len); // output optimized packed stream // repeat run are encoded cc xx // copy run are encoded 00 cc xx xx xx xx i = 0; src = 0; while (counts[i] != 0) { while (counts[i] & 0x8000) // repeat run { count = counts[i] & ~0x8000; GFX2_Log(GFX2_DEBUG, "MOTO_MAP_pack() %4u %4u repeat %u times %02x\n", src, i, count, unpacked[src]); while(count > 255) { packed[dst++] = 255; packed[dst++] = unpacked[src]; count -= 255; src += 255; } packed[dst++] = count; packed[dst++] = unpacked[src]; src += count; i++; } while (counts[i] != 0 && !(counts[i] & 0x8000)) // copy run { // calculate the "savings" of repeat runs between 2 copy run int savings = 0; unsigned int j; GFX2_Log(GFX2_DEBUG, "MOTO_MAP_pack() %4u %4u copy %u bytes\n", src, i, counts[i]); for (j = i + 1; counts[j] & 0x8000; j++) // check repeat runs until the next copy run { count = counts[j] & ~0x8000; if (savings < 0 && (savings + (int)count - 2) > 0) break; savings += count - 2; // a repeat run outputs 2 bytes for count bytes of input } count = counts[i]; GFX2_Log(GFX2_DEBUG, " savings=%d i=%u j=%u (counts[j]=0x%04x)\n", savings, i, j, counts[j]); if (savings < 2 && (j > i + 1)) { unsigned int k; if (counts[j] == 0) // go to the end of stream { for (k = i + 1; k < j; k++) count += (counts[k] & ~0x8000); GFX2_Log(GFX2_DEBUG, "MOTO_MAP_pack() src=%u extend copy from %u to %u\n", src, counts[i], count); i = j - 1; } else { for (k = i + 1; k < j; k++) count += (counts[k] & ~0x8000); if (!(counts[j] & 0x8000)) { // merge with the next copy run (and the repeat runs between) GFX2_Log(GFX2_DEBUG, "MOTO_MAP_pack() src=%u merge savings=%d\n", src, savings); i = j; counts[i] += count; continue; } else { // merge with the next few repeat runs GFX2_Log(GFX2_DEBUG, "MOTO_MAP_pack() src=%u extends savings=%d\n", src, savings); i = j - 1; } } } while (count > 255) { packed[dst++] = 0; packed[dst++] = 255; memcpy(packed+dst, unpacked+src, 255); dst += 255; src += 255; count -= 255; } packed[dst++] = 0; packed[dst++] = count; memcpy(packed+dst, unpacked+src, count); dst += count; src += count; i++; } } free(counts); return dst; #endif } /** * GUI window to choose Thomson MO/TO saving parameters * * @param[out] machine target machine * @param[out] format file format (0 = BIN, 1 = MAP) * @param[in,out] mode video mode @ref MOTO_Graphic_Mode */ static int Save_MOTO_window(enum MOTO_Machine_Type * machine, int * format, enum MOTO_Graphic_Mode * mode) { int button; T_Dropdown_button * machine_dd; T_Dropdown_button * format_dd; T_Dropdown_button * mode_dd; static const char * mode_list[] = { "40col", "80col", "bm4", "bm16" }; char text_info[24]; Open_window(200, 125, "Thomson MO/TO Saving"); Window_set_normal_button(110,100,80,15,"Save",1,1,KEY_RETURN); // 1 Window_set_normal_button(10,100,80,15,"Cancel",1,1,KEY_ESCAPE); // 2 Print_in_window(13,18,"Target Machine:",MC_Dark,MC_Light); machine_dd = Window_set_dropdown_button(10,28,110,15,100, (*mode == MOTO_MODE_40col) ? "TO7/TO7-70" : "TO9/TO8/TO9+", 1, 0, 1, LEFT_SIDE,0); // 3 if (*mode == MOTO_MODE_40col) Window_dropdown_add_item(machine_dd, MACHINE_TO7, "TO7/TO7-70"); Window_dropdown_add_item(machine_dd, MACHINE_TO8, "TO9/TO8/TO9+"); if (*mode == MOTO_MODE_40col) Window_dropdown_add_item(machine_dd, MACHINE_MO5, "MO5"); Window_dropdown_add_item(machine_dd, MACHINE_MO6, "MO6"); Print_in_window(13,46,"Format:",MC_Dark,MC_Light); format_dd = Window_set_dropdown_button(10,56,110,15,92,"BIN",1, 0, 1, LEFT_SIDE,0); // 4 Window_dropdown_add_item(format_dd, 0, "BIN"); Window_dropdown_add_item(format_dd, 1, "MAP/TO-SNAP"); Print_in_window(136,46,"Mode:",MC_Dark,MC_Light); mode_dd = Window_set_dropdown_button(136,56,54,15,44,mode_list[*mode],1, 0, 1, LEFT_SIDE,0); // 5 if (*mode == MOTO_MODE_40col) Window_dropdown_add_item(mode_dd, *mode, mode_list[*mode]); if (*mode == MOTO_MODE_80col) Window_dropdown_add_item(mode_dd, *mode, mode_list[*mode]); if (*mode == MOTO_MODE_40col) Window_dropdown_add_item(mode_dd, MOTO_MODE_bm4, mode_list[MOTO_MODE_bm4]); if (*mode == MOTO_MODE_bm16) Window_dropdown_add_item(mode_dd, *mode, mode_list[*mode]); Update_window_area(0,0,Window_width,Window_height); Display_cursor(); do { button = Window_clicked_button(); if (Is_shortcut(Key, 0x100+BUTTON_HELP)) { Key = 0; Window_help(BUTTON_SAVE, "THOMSON MO/TO FORMAT"); } else switch (button) { case 3: *machine = (enum MOTO_Machine_Type)Window_attribute2; break; case 4: *format = Window_attribute2; break; case 5: *mode = (enum MOTO_Graphic_Mode)Window_attribute2; break; } Hide_cursor(); //"ABCDEFGHIJKLMNOPQRSTUVW" memset(text_info, ' ', 23); text_info[23] = '\0'; if (*machine == MACHINE_TO7 || *machine == MACHINE_TO770 || *machine == MACHINE_MO5) { if (*mode != MOTO_MODE_40col) snprintf(text_info, sizeof(text_info), "%s only supports 40col", (*machine == MACHINE_MO5) ? "MO5" : "TO7"); else if (*format == 1) strncpy(text_info, "No TO-SNAP extension. ", sizeof(text_info)); else strncpy(text_info, "No palette to save. ", sizeof(text_info)); } Print_in_window(9, 80, text_info, MC_Dark, MC_Light); Display_cursor(); } while(button!=1 && button!=2); Close_window(); Display_cursor(); return button==1; } /** * Save a picture in MAP or BIN Thomson MO/TO file format. * * File format details : * http://pulkomandy.tk/projects/GrafX2/wiki/Develop/FileFormats/MOTO */ void Save_MOTO(T_IO_Context * context) { int transpose = 1; // transpose upper bits in "couleur" vram enum MOTO_Machine_Type target_machine = MACHINE_TO7; int format = 0; // 0 = BIN, 1 = MAP enum MOTO_Graphic_Mode mode; FILE * file = NULL; byte * vram_forme; byte * vram_couleur; int i, x, y, bx; word reg_prc = 0xE7C3; // PRC : TO7/8/9 0xE7C3 ; MO5/MO6 0xA7C0 byte prc_value = 0x65;// Value to write to PRC to select VRAM bank // MO5 : 0x51 word vram_address = 0x4000; // 4000 on TO7/TO8/TO9, 0000 on MO5/MO6 File_error = 1; /** * In the future we could support other resolution for .MAP * format. * And even in .BIN format, we could store less lines. */ if (context->Height != 200) { Warning_message("must be 640x200, 320x200 or 160x200"); return; } switch (context->Width) { case 160: mode = MOTO_MODE_bm16; target_machine = MACHINE_TO8; break; case 640: mode = MOTO_MODE_80col; target_machine = MACHINE_TO8; break; case 320: mode = MOTO_MODE_40col; // or bm4 break; default: Warning_message("must be 640x200, 320x200 or 160x200"); return; } if (!Save_MOTO_window(&target_machine, &format, &mode)) return; if (target_machine == MACHINE_MO5 || target_machine == MACHINE_MO6) { reg_prc = 0xA7C0; // PRC : MO5/MO6 0xA7C0 prc_value = 0x51; vram_address = 0; transpose = 0; } vram_forme = GFX2_malloc(8192); vram_couleur = GFX2_malloc(8192); switch (mode) { case MOTO_MODE_40col: { /** * The 40col encoding algorithm is optimized for further vertical * RLE packing. The "attibute" byte is kept as constant as possible * between adjacent blocks. */ unsigned color_freq[16]; unsigned max_freq = 0; byte previous_fond = 0, previous_forme = 0; byte most_used_color = 0; // search for most used color to prefer it as background color for (i = 0; i < 16; i++) color_freq[i] = 0; for (y = 0; y < context->Height; y++) { for (x = 0; x < context->Width; x++) { byte col = Get_pixel(context, x, y); if (col > 15) { Warning_with_format("color %u > 15 at pixel (%d,%d)", col, x, y); goto error; } color_freq[col]++; } } for (i = 0; i < 16; i++) { if (color_freq[i] > max_freq) { max_freq = color_freq[i]; most_used_color = (byte)i; // most used color } } previous_fond = most_used_color; max_freq = 0; for (i = 0; i < 16; i++) { if (i != most_used_color && color_freq[i] > max_freq) { max_freq = color_freq[i]; previous_forme = (byte)i; // second most used color } } GFX2_Log(GFX2_DEBUG, "Save_MOTO() most used color index %u, 2nd %u\n", previous_fond, previous_forme); if (target_machine == MACHINE_MO5) { /** * For MO5 we use a different 40col algorithm * to make sure the last pixel of a GPL and the first the next * are both FORME or both FOND, else we get an ugly glitch on the * EFGJ033 Gate Array MO5! */ byte forme_byte = 0; byte couleur_byte = 0x10; GFX2_Log(GFX2_DEBUG, "Save_MOTO() 40col using MO5 algo\n"); for (y = 0; y < context->Height; y++) { for (bx = 0; bx < 40; bx++) { byte fond = 0xff, forme = 0xff; forme_byte &= 1; // Last bit of the previous FORME byte x = bx*8; if (forme_byte) forme = Get_pixel(context, x, y); else fond = Get_pixel(context, x, y); while (++x < bx * 8 + 8) { byte col = Get_pixel(context, x, y); forme_byte <<= 1; if (col == forme) forme_byte |= 1; else if (col != fond) { if (forme == 0xff) { forme_byte |= 1; forme = col; } else if (fond == 0xff) fond = col; else { Warning_with_format("Constraint error at pixel (%d,%d)", x, y); goto error; } } } if (forme != 0xff) couleur_byte = (forme << 4) | (couleur_byte & 0x0f); if (fond != 0xff) couleur_byte = (couleur_byte & 0xf0) | fond; vram_forme[bx+y*40] = forme_byte; vram_couleur[bx+y*40] = couleur_byte; } } } else { GFX2_Log(GFX2_DEBUG, "Save_MOTO() 40col using optimized algo\n"); // encoding of each 8x1 block for (bx = 0; bx < 40; bx++) { for (y = 0; y < context->Height; y++) { byte forme_byte = 1; byte col; byte c1, c1_count = 1; byte c2 = 0xff, c2_count = 0; byte fond, forme; x = bx * 8; c1 = Get_pixel(context, x, y); while (++x < bx * 8 + 8) { forme_byte <<= 1; col = Get_pixel(context, x, y); if (col > 15) { Warning_with_format("color %d > 15 at pixel (%d,%d)", col, x, y); goto error; } if (col == c1) { forme_byte |= 1; c1_count++; } else { c2_count++; if (c2 == 0xff) c2 = col; else if (col != c2) { Warning_with_format("constraint error at pixel (%d,%d)", x, y); goto error; } } } if (c2 == 0xff) { // Only one color in the 8x1 block if (c1 == previous_fond) c2 = previous_forme; else c2 = previous_fond; } // select background color (fond) // and foreground color (forme) if (c1 == previous_fond) { fond = c1; forme = c2; forme_byte = ~forme_byte; } else if (c2 == previous_fond) { fond = c2; forme = c1; } else if (c1 == most_used_color) { fond = c1; forme = c2; forme_byte = ~forme_byte; } else if (c2 == most_used_color) { fond = c2; forme = c1; } else if (c1_count >= c2_count) { fond = c1; forme = c2; forme_byte = ~forme_byte; } else { fond = c2; forme = c1; } // write to VRAM vram_forme[bx+y*40] = forme_byte; // transpose for TO7 compatibility if (transpose) vram_couleur[bx+y*40] = ((fond & 7) | ((fond & 8) << 4) | (forme << 3)) ^ 0xC0; else vram_couleur[bx+y*40] = fond | (forme << 4); previous_fond = fond; previous_forme = forme; } if (transpose) { previous_fond = (vram_couleur[bx] & 7) | (~vram_couleur[bx] & 0x80) >> 4; previous_forme = ((vram_couleur[bx] & 0x78) >> 3) ^ 8; } else { previous_fond = vram_couleur[bx] & 15; previous_forme = vram_couleur[bx] >> 4; } } } } break; case MOTO_MODE_80col: for (bx = 0; bx < context->Width / 16; bx++) { for (y = 0; y < context->Height; y++) { byte val = 0; for (x = bx * 16; x < bx*16 + 8; x++) val = (val << 1) | Get_pixel(context, x, y); vram_forme[y*(context->Width/16)+bx] = val; for (; x < bx*16 + 16; x++) val = (val << 1) | Get_pixel(context, x, y); vram_couleur[y*(context->Width/16)+bx] = val; } } break; case MOTO_MODE_bm4: for (y = 0; y < context->Height; y++) { for (bx = 0; bx < context->Width / 8; bx++) { byte val1 = 0, val2 = 0, pixel; for (x = bx * 8; x < bx*8 + 8; x++) { pixel = Get_pixel(context, x, y); if (pixel > 3) { Warning_with_format("color %d > 3 at pixel (%d,%d)", pixel, x, y); goto error; } val1 = (val1 << 1) | (pixel >> 1); val2 = (val2 << 1) | (pixel & 1); } vram_forme[y*(context->Width/8)+bx] = val1; vram_couleur[y*(context->Width/8)+bx] = val2; } } break; case MOTO_MODE_bm16: for (bx = 0; bx < context->Width / 4; bx++) { for (y = 0; y < context->Height; y++) { vram_forme[y*(context->Width/4)+bx] = (Get_pixel(context, bx*4, y) << 4) | Get_pixel(context, bx*4+1, y); vram_couleur[y*(context->Width/4)+bx] = (Get_pixel(context, bx*4+2, y) << 4) | Get_pixel(context, bx*4+3, y); } } break; } // palette for (i = 0; i < 16; i++) { word to8color = MOTO_gamma_correct_RGB_to_MOTO(context->Palette + i); vram_forme[8000+i*2] = to8color >> 8; vram_forme[8000+i*2+1] = to8color & 0xFF; } file = Open_file_write(context); if (file == NULL) goto error; if (format == 0) // BIN { word chunk_length; if (target_machine == MACHINE_TO7 || target_machine == MACHINE_TO770 || target_machine == MACHINE_MO5) chunk_length = 8000; // Do not save palette else { chunk_length = 8000 + 32 + 32; // data + palette + comment // Commentaire if (context->Comment[0] != '\0') strncpy((char *)vram_forme + 8032, context->Comment, 32); else snprintf((char *)vram_forme + 8032, 32, "GrafX2 %s.%s", Program_version, SVN_revision); // also saves the video mode vram_forme[8063] = '0' + mode; memcpy(vram_couleur + 8000, vram_forme + 8000, 64); } // Format BIN // TO8/TO9 : set LGAMOD 0xE7DC 40col=0 bm4=0x21 80col=0x2a bm16=0x7b if (!DECB_BIN_Add_Chunk(file, 1, reg_prc, &prc_value)) goto error; if (!DECB_BIN_Add_Chunk(file, chunk_length, vram_address, vram_forme)) goto error; prc_value &= 0xFE; // select color data if (!DECB_BIN_Add_Chunk(file, 1, reg_prc, &prc_value)) goto error; if (!DECB_BIN_Add_Chunk(file, chunk_length, vram_address, vram_couleur)) goto error; if (!DECB_BIN_Add_End(file, 0x0000)) goto error; } else { // format MAP with TO-SNAP extensions byte * unpacked_data; byte * packed_data; unpacked_data = GFX2_malloc(16*1024); packed_data = GFX2_malloc(16*1024); if (packed_data == NULL || unpacked_data == NULL) { GFX2_Log(GFX2_ERROR, "Failed to allocate 2x16kB of memory\n"); free(packed_data); free(unpacked_data); goto error; } switch (mode) { case MOTO_MODE_40col: case MOTO_MODE_bm4: packed_data[0] = 0; // mode packed_data[1] = (context->Width / 8) - 1; break; case MOTO_MODE_80col: packed_data[0] = 0x80; // mode packed_data[1] = (context->Width / 8) - 1; break; case MOTO_MODE_bm16: packed_data[0] = 0x40; // mode packed_data[1] = (context->Width / 2) - 1; break; } packed_data[2] = (context->Height / 8) - 1; // 1st step : put data to pack in a linear buffer // 2nd step : pack data i = 0; switch (mode) { case MOTO_MODE_40col: case MOTO_MODE_bm4: for (bx = 0; bx <= packed_data[1]; bx++) { for (y = 0; y < context->Height; y++) { unpacked_data[i] = vram_forme[bx + y*(packed_data[1]+1)]; unpacked_data[i+8192] = vram_couleur[bx + y*(packed_data[1]+1)]; i++; } } i = 3; i += MOTO_MAP_pack(packed_data+3, unpacked_data, context->Height * (packed_data[1]+1)); packed_data[i++] = 0; // ending of VRAM forme packing packed_data[i++] = 0; i += MOTO_MAP_pack(packed_data+i, unpacked_data + 8192, context->Height * (packed_data[1]+1)); packed_data[i++] = 0; // ending of VRAM couleur packing packed_data[i++] = 0; break; case MOTO_MODE_80col: case MOTO_MODE_bm16: for (bx = 0; bx < (packed_data[1] + 1) / 2; bx++) { for (y = 0; y < context->Height; y++) unpacked_data[i++] = vram_forme[bx + y*(packed_data[1]+1)/2]; for (y = 0; y < context->Height; y++) unpacked_data[i++] = vram_couleur[bx + y*(packed_data[1]+1)/2]; } i = 3; i += MOTO_MAP_pack(packed_data+3, unpacked_data, context->Height * (packed_data[1]+1)); packed_data[i++] = 0; // ending of VRAM forme packing packed_data[i++] = 0; packed_data[i++] = 0; // ending of VRAM couleur packing packed_data[i++] = 0; break; } if (i&1) // align packed_data[i++] = 0; if (target_machine != MACHINE_TO7 && target_machine != MACHINE_TO770 && target_machine != MACHINE_MO5) { // add TO-SNAP extension // see http://collection.thomson.free.fr/code/articles/prehisto_bulletin/page.php?XI=0&XJ=13 // bytes 0-1 : Hardware video mode (value of SCRMOD 0x605F) packed_data[i++] = 0; switch (mode) { case MOTO_MODE_40col: packed_data[i++] = 0; break; case MOTO_MODE_bm4: packed_data[i++] = 0x01; break; case MOTO_MODE_80col: packed_data[i++] = 0x80; break; case MOTO_MODE_bm16: packed_data[i++] = 0x40; break; } // bytes 2-3 : Border color packed_data[i++] = 0; packed_data[i++] = 0; // bytes 4-5 : BASIC video mode (CONSOLE,,,,X) packed_data[i++] = 0; switch (mode) { case MOTO_MODE_40col: packed_data[i++] = 0; break; case MOTO_MODE_bm4: packed_data[i++] = 2; break; case MOTO_MODE_80col: packed_data[i++] = 1; break; case MOTO_MODE_bm16: packed_data[i++] = 3; break; } // bytes 6-37 : BGR palette for (x = 0; x < 16; x++) { word bgr = MOTO_gamma_correct_RGB_to_MOTO(context->Palette + x); packed_data[i++] = bgr >> 8; packed_data[i++] = bgr & 0xff; } // bytes 38-39 : TO-SNAP signature packed_data[i++] = 0xA5; packed_data[i++] = 0x5A; } free(unpacked_data); if (!DECB_BIN_Add_Chunk(file, i, 0, packed_data) || !DECB_BIN_Add_End(file, 0x0000)) { free(packed_data); goto error; } free(packed_data); } fclose(file); File_error = 0; return; error: free(vram_forme); free(vram_couleur); if (file) fclose(file); File_error = 1; }