/** * @file frame.cpp * @author giomba@glgprograms.it * @brief Transform a black and white PNM image into the assembly code for the video generator. * * Takes a black/white (1bpp) PNM ASCII-armored image as input, * and produces the frame.S assembly code to draw it * as an analog video signal for this project's kernel. * * @copyright Copyright retrofficina.glgprograms.it (c) 2021-2022 * */ #include #include #include #include #include enum ExitStatus { BAD_ARGUMENT = 1, FILE_NOT_OPEN = 2, FILE_BAD_FORMAT = 3 }; int main(int argc, char **argv) { // enable debug messages spdlog::set_level(spdlog::level::debug); // arguments parsing and sanity check if (argc < 2) { spdlog::error("Usage: {} ...", argv[0]); return BAD_ARGUMENT; } const int images = argc - 1; // Output assembly file content. // Content is buffered into these stringstreams, // and then dumped to a file on disk. // Code is organized into two parts. std::stringstream jump_table; // Assembly jump table for images. // Each image is described by a jump table with 256 entries. // Each entry is a jump to the code to produce a particular line on the screen. std::stringstream asm_code; // Assembly code to describe lines. // Each line is a sequence of a set and/or clear bit, // conveniently timed to mangle the video signal at the right moment. // Output file. std::fstream outfile; outfile.open("frame.S", std::ios::out); if (!outfile) return FILE_NOT_OPEN; // include headers jump_table << "#include \"macro.h\"" << std::endl; jump_table << "#include " << std::endl; // Pointer to images char **image = new char *[images]; // Parse images and convert in assembly code for (int current_image = 0; current_image < images; ++current_image) { std::fstream infile; infile.open(argv[1 + current_image], std::ios::in); if (!infile) return FILE_NOT_OPEN; unsigned int width, height; // width and height of image std::string format; // image format char c; // current pixel getline(infile, format); spdlog::debug("format: {}", format); if (format != "P1") return FILE_BAD_FORMAT; // skip commented line / TODO in a proper way getline(infile, format); spdlog::debug("comment: {}", format); // read width and height infile >> width >> height; spdlog::debug("width x height = {} x {}", width, height); // read full image from disk into memory // in an organized row by column array image[current_image] = new char[height * width]; for (int y = 0; y < height;) { for (int x = 0; x < width;) { c = infile.get(); // file is truncated if (!infile.good()) return FILE_BAD_FORMAT; // if current character represents a pixel, add to array... if (c == '0' || c == '1') { image[current_image][y * width + x] = c; x++; } else // ... else ignore current char continue; } y++; } infile.close(); // add new jump table for current image jump_table << ".global line_jump_table_" << current_image << std::endl; jump_table << "line_jump_table_" << current_image << ":" << std::endl; // Inspect each line one by one for (int y = 0; y < height; ++y) { int diff = 1; // Check if one of the previously inspected images has an identical line. // If it has an identical line, just add it in the current image jump table, // instead of generating again an identical assembly code. // This allows us to save a lot of program memory space. // Note: previous lines of current image are good candidates to check too! for (int other_image = 0; other_image <= current_image; ++other_image) { for (int other_y = 0; other_y < height && other_y < y; ++other_y) { diff = memcmp(&(image[current_image][y * width + 0]), &(image[other_image][other_y * width + 0]), width); if (diff == 0) { jump_table << '\t' << "jmp line_" << other_image << "_" << other_y << std::endl; goto nested_loop_end; } } } nested_loop_end: // Current line has already been found to be identical // to one of the previous ones: then skip to next one. if (diff == 0) continue; // Add jump in the current image table. jump_table << '\t' << "jmp line_" << current_image << "_" << y << std::endl; // Add line description. asm_code << "line_" << current_image << "_" << y << ":" << std::endl; // Line description is a sequence of assembly code conveniently built // in order to set/clear an output port bit at the right moment, // and mangle the video signal in order to display the desired image. // Code simulates an RLE scheme, exploting small conveniently timed loops, // in order to save program space. int count = 0; char last = image[current_image][y * width + 0]; for (int x = 0; x < width; ++x) { char current = image[current_image][y * width + x]; // If current pixel is the same as previous one, // then just count++ if (current == last) ++count; else // current pixel is different from previous one { // Each pixel is 4 clock cycles long, // because It Just Works out perfectly with the instruction set. // Two clock cycles are spent to set/clear the output port. if (last == '0') { asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl; // +2 } else if (last == '1') { asm_code << '\t' << "sbi IO(PORTB), 4" << std::endl; // +2 } // Loop is entered only if RLE is needed, // eg. at least 2 subsequent pixels of the same color. if (count > 1) { // clock cycles asm_code << '\t' << "ldi r31, " << count - 1 << std::endl; // +1 asm_code << "1:" << std::endl; // +0 asm_code << '\t' << "dec r31" << std::endl; // +1 asm_code << '\t' << "nop" << std::endl; // +1 asm_code << '\t' << "brne 1b" << std::endl; // +2 if taken, +1 if not } // Add 2 more dummy clock cycles. asm_code << '\t' << "rjmp 2f" << std::endl; // +2 asm_code << "2:" << std::endl; last = current; count = 1; } } // At the end of the line, clear output bit, and return from interrupt. asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl; asm_code << '\t' << "jmp jump_table_return_address" << std::endl; } } delete[] image; // save the generated code on disk outfile << jump_table.str() << asm_code.str(); outfile.close(); }