├── Makefile ├── vga_timings.hpp ├── main.cpp └── gif.h /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES = main.cpp 2 | TOP_MODULE:=$(shell awk -F'"' '/top_module:/ {print $$2}' ../info.yaml) 3 | VERILOG_SOURCES:=$(shell sed -n 's/.*- "\(.*\)".*/..\/src\/\1/p' ../info.yaml | xargs) 4 | OBJ_DIR:=obj_dir 5 | LANGUAGE:=--default-language 1364-2005 6 | 7 | VFLAGS = -Wall -Wpedantic $(LANGUAGE) --x-assign fast --x-initial fast --noassert --top-module $(TOP_MODULE) 8 | CFLAGS = -flto -O3 -march=native -DTOP_MODULE=V$(TOP_MODULE) -I$(OBJ_DIR) -I/usr/share/verilator/include -include V$(TOP_MODULE).h 9 | LDFLAGS = -flto -lSDL2 10 | 11 | all: $(OBJ_DIR)/V$(TOP_MODULE).h 12 | make -C $(OBJ_DIR) -f V$(TOP_MODULE).mk 13 | 14 | $(OBJ_DIR)/V$(TOP_MODULE).h: $(VERILOG_SOURCES) $(SOURCES) 15 | verilator $(VFLAGS) --cc $(VERILOG_SOURCES) --exe $(SOURCES) -CFLAGS "$(CFLAGS)" -LDFLAGS "$(LDFLAGS)" 16 | 17 | lint: $(VERILOG_SOURCES) 18 | verilator --lint-only $(VFLAGS) $(VERILOG_SOURCES) 19 | 20 | sim: all 21 | $(OBJ_DIR)/V$(TOP_MODULE) 22 | 23 | gif: all 24 | $(OBJ_DIR)/V$(TOP_MODULE) --gif 1024 25 | 26 | clean: 27 | rm -rf $(OBJ_DIR) 28 | rm -f output.gif 29 | 30 | distclean: clean 31 | 32 | .PHONY: all lint sim gif clean distclean 33 | -------------------------------------------------------------------------------- /vga_timings.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* 5 | * Timings found at: http://www.tinyvga.com/vga-timing 6 | */ 7 | 8 | // Packed data struct for efficient storage 9 | struct vga_timing { 10 | float clock_mhz; 11 | uint64_t h_active_pixels : 11; 12 | uint64_t h_front_porch : 8; 13 | uint64_t h_sync_pulse : 8; 14 | uint64_t h_back_porch : 9; 15 | uint64_t h_sync_pol : 1; 16 | uint64_t v_active_lines : 11; 17 | uint64_t v_front_porch : 6; 18 | uint64_t v_sync_pulse : 3; 19 | uint64_t v_back_porch : 6; 20 | uint64_t v_sync_pol : 1; 21 | uint64_t frame_cycles() const { return (h_active_pixels + h_front_porch + h_sync_pulse + h_back_porch) * (v_active_lines + v_front_porch + v_sync_pulse + v_back_porch); } 22 | } __attribute__((packed)); 23 | 24 | enum vga_sync { 25 | VGA_SYNC_NEG, 26 | VGA_SYNC_POS 27 | }; 28 | 29 | enum vga_format { 30 | VGA_640_350_70, 31 | VGA_640_350_85, 32 | VGA_640_400_70, 33 | VGA_640_400_85, 34 | VGA_640_480_60, 35 | VGA_640_480_72, 36 | VGA_640_480_75, 37 | VGA_640_480_85, 38 | VGA_720_400_85, 39 | VGA_768_576_60, 40 | VGA_768_576_72, 41 | VGA_768_576_75, 42 | VGA_768_576_85, 43 | VGA_800_600_56, 44 | VGA_800_600_60, 45 | VGA_800_600_72, 46 | VGA_800_600_75, 47 | VGA_800_600_85, 48 | VGA_1024_768_60, 49 | VGA_1024_768_70, 50 | VGA_1024_768_75, 51 | VGA_1024_768_85, 52 | VGA_1152_864_75, 53 | VGA_1280_960_60, 54 | VGA_1280_960_85, 55 | VGA_1280_1024_60, 56 | VGA_1280_1024_75, 57 | VGA_1280_1024_85, 58 | VGA_1600_1200_60, 59 | VGA_1600_1200_65, 60 | VGA_1600_1200_70, 61 | VGA_1600_1200_75, 62 | VGA_1600_1200_85, 63 | VGA_NUM_FORMAT 64 | }; 65 | 66 | constexpr std::array vga_timings = {{ 67 | [ VGA_640_350_70] = { 25.175, 640, 16, 96, 48, VGA_SYNC_POS, 350, 37, 2, 60, VGA_SYNC_NEG}, 68 | [ VGA_640_350_85] = { 31.500, 640, 32, 64, 96, VGA_SYNC_POS, 350, 32, 3, 60, VGA_SYNC_NEG}, 69 | [ VGA_640_400_70] = { 25.175, 640, 16, 96, 48, VGA_SYNC_NEG, 400, 12, 2, 34, VGA_SYNC_POS}, 70 | [ VGA_640_400_85] = { 31.500, 640, 32, 64, 96, VGA_SYNC_NEG, 400, 1, 3, 41, VGA_SYNC_POS}, 71 | [ VGA_640_480_60] = { 25.175, 640, 16, 96, 48, VGA_SYNC_NEG, 480, 10, 2, 33, VGA_SYNC_NEG}, 72 | [ VGA_640_480_72] = { 31.500, 640, 24, 40, 128, VGA_SYNC_NEG, 480, 9, 2, 29, VGA_SYNC_POS}, 73 | [ VGA_640_480_75] = { 31.500, 640, 16, 64, 120, VGA_SYNC_NEG, 480, 1, 3, 16, VGA_SYNC_NEG}, 74 | [ VGA_640_480_85] = { 36.000, 640, 56, 56, 80, VGA_SYNC_NEG, 480, 1, 3, 25, VGA_SYNC_NEG}, 75 | [ VGA_720_400_85] = { 35.500, 720, 36, 72, 108, VGA_SYNC_NEG, 400, 1, 3, 42, VGA_SYNC_POS}, 76 | [ VGA_768_576_60] = { 34.960, 768, 24, 80, 104, VGA_SYNC_NEG, 576, 1, 3, 17, VGA_SYNC_POS}, 77 | [ VGA_768_576_72] = { 42.930, 768, 32, 80, 112, VGA_SYNC_NEG, 576, 1, 3, 21, VGA_SYNC_POS}, 78 | [ VGA_768_576_75] = { 45.510, 768, 40, 80, 120, VGA_SYNC_NEG, 576, 1, 3, 22, VGA_SYNC_POS}, 79 | [ VGA_768_576_85] = { 51.840, 768, 40, 80, 120, VGA_SYNC_NEG, 576, 1, 3, 25, VGA_SYNC_POS}, 80 | [ VGA_800_600_56] = { 36.000, 800, 24, 72, 128, VGA_SYNC_POS, 600, 1, 2, 22, VGA_SYNC_POS}, 81 | [ VGA_800_600_60] = { 40.000, 800, 40, 128, 88, VGA_SYNC_POS, 600, 1, 4, 23, VGA_SYNC_POS}, 82 | [ VGA_800_600_72] = { 50.000, 800, 56, 120, 64, VGA_SYNC_POS, 600, 37, 6, 23, VGA_SYNC_POS}, 83 | [ VGA_800_600_75] = { 49.500, 800, 16, 80, 160, VGA_SYNC_POS, 600, 1, 3, 21, VGA_SYNC_POS}, 84 | [ VGA_800_600_85] = { 56.250, 800, 32, 64, 152, VGA_SYNC_POS, 600, 1, 3, 27, VGA_SYNC_POS}, 85 | [ VGA_1024_768_60] = { 65.000, 1024, 24, 136, 160, VGA_SYNC_NEG, 768, 3, 6, 29, VGA_SYNC_NEG}, 86 | [ VGA_1024_768_70] = { 75.000, 1024, 24, 136, 144, VGA_SYNC_NEG, 768, 3, 6, 29, VGA_SYNC_NEG}, 87 | [ VGA_1024_768_75] = { 78.800, 1024, 16, 96, 176, VGA_SYNC_POS, 768, 1, 3, 28, VGA_SYNC_POS}, 88 | [ VGA_1024_768_85] = { 94.500, 1024, 48, 96, 208, VGA_SYNC_POS, 768, 1, 3, 36, VGA_SYNC_POS}, 89 | [ VGA_1152_864_75] = {108.000, 1152, 64, 128, 256, VGA_SYNC_POS, 864, 1, 3, 32, VGA_SYNC_POS}, 90 | [ VGA_1280_960_60] = {108.000, 1280, 96, 112, 312, VGA_SYNC_POS, 960, 1, 3, 36, VGA_SYNC_POS}, 91 | [ VGA_1280_960_85] = {148.500, 1280, 64, 160, 224, VGA_SYNC_POS, 960, 1, 3, 47, VGA_SYNC_POS}, 92 | [VGA_1280_1024_60] = {108.000, 1280, 48, 112, 248, VGA_SYNC_POS, 1024, 1, 3, 38, VGA_SYNC_POS}, 93 | [VGA_1280_1024_75] = {135.000, 1280, 16, 144, 248, VGA_SYNC_POS, 1024, 1, 3, 38, VGA_SYNC_POS}, 94 | [VGA_1280_1024_85] = {157.500, 1280, 64, 160, 224, VGA_SYNC_POS, 1024, 1, 3, 44, VGA_SYNC_POS}, 95 | [VGA_1600_1200_60] = {162.000, 1600, 64, 192, 304, VGA_SYNC_POS, 1200, 1, 3, 46, VGA_SYNC_POS}, 96 | [VGA_1600_1200_65] = {175.500, 1600, 64, 192, 304, VGA_SYNC_POS, 1200, 1, 3, 46, VGA_SYNC_POS}, 97 | [VGA_1600_1200_70] = {189.000, 1600, 64, 192, 304, VGA_SYNC_POS, 1200, 1, 3, 46, VGA_SYNC_POS}, 98 | [VGA_1600_1200_75] = {202.500, 1600, 64, 192, 304, VGA_SYNC_POS, 1200, 1, 3, 46, VGA_SYNC_POS}, 99 | [VGA_1600_1200_85] = {229.500, 1600, 64, 192, 304, VGA_SYNC_POS, 1200, 1, 3, 46, VGA_SYNC_POS} 100 | }}; 101 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "verilated.h" 7 | #include "vga_timings.hpp" 8 | #include "gif.h" 9 | 10 | struct ABGR8888_t { uint8_t r, g, b, a; } __attribute__((packed)); 11 | union VGApinout_t { 12 | uint8_t pins; 13 | struct { // 6-bit color with sync 14 | uint8_t r1 :1; uint8_t g1 :1; uint8_t b1 :1; uint8_t vsync :1; 15 | uint8_t r0 :1; uint8_t g0 :1; uint8_t b0 :1; uint8_t hsync :1; 16 | } __attribute__((packed)); 17 | }; 18 | 19 | int main(int argc, char **argv) 20 | { 21 | static Uint32 fullscreen = 0; // Defaul command line options 22 | bool polarity = false, slow = false, gif = false; 23 | int gif_frames = 0; 24 | std::vector modes{VGA_640_480_60, VGA_768_576_60, VGA_800_600_60, VGA_1024_768_60}; 25 | vga_timing mode = vga_timings[modes[0]]; 26 | 27 | for (int i = 1; i < argc; i++) { // Handle command line arguments 28 | char* p = argv[i]; 29 | if (!strcmp("--", p)) break; 30 | else if (!strcmp("--fullscreen", p)) fullscreen = fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; 31 | else if (!strcmp("--polarity", p)) polarity = !polarity; 32 | else if (!strcmp("--slow", p)) slow = !slow; 33 | else if (!strcmp("--mode", p)) { 34 | if (i + 1 < argc) { 35 | int m = atoi(argv[++i]); 36 | if (m >= 0 && m < modes.size()) mode = vga_timings[modes[m]]; 37 | } 38 | } else if (!strcmp("--gif", p)) { 39 | gif = !gif; 40 | if (i + 1 < argc) gif_frames = atoi(argv[++i]); 41 | } else { 42 | printf("Command Line | [Key]\n"); 43 | printf(" --fullscreen | [ F ]\tToggles SDL window size (default: %s)\n", fullscreen ? "maximized" : "minimized"); 44 | printf(" --polarity | [ P ]\tToggles the VGA polarity sync high/low (default: %s)\n", polarity ? "true" : "false"); 45 | printf(" --slow | [ S ]\tToggles the displayed frame rate (default: %s)\n", slow ? "true" : "false"); 46 | printf(" --mode [#] \tSets SDL VGA timing mode (value: [0:%ld])\n", modes.size()-1); 47 | printf(" --gif [#frames] \tSaves animated GIF (default: %s [%d])\n", gif ? "true" : "false", gif_frames); 48 | printf(" | [ Q ]\tQuits/Escapes (stops GIF if enabled).\n"); 49 | return 1; 50 | } 51 | } 52 | 53 | vga_timing vga = mode; // Select the VGA timings from the list 54 | std::vector fb(vga.h_active_pixels * vga.v_active_lines); 55 | 56 | GifWriter g; // GIF output 57 | int delay = ceilf(vga.frame_cycles() / (vga.clock_mhz * 10000.f)); // 100ths of a second 58 | if (gif) GifBegin(&g, "output.gif", vga.h_active_pixels, vga.v_active_lines, delay); 59 | 60 | SDL_Init(SDL_INIT_VIDEO); // Initialize SDL2 61 | SDL_Window* w = SDL_CreateWindow("Tiny Tapeout VGA", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, vga.h_active_pixels, vga.v_active_lines, SDL_WINDOW_RESIZABLE | fullscreen); 62 | SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best"); 63 | SDL_Renderer* r = SDL_CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED); 64 | SDL_RenderSetLogicalSize(r, vga.h_active_pixels, vga.v_active_lines); 65 | SDL_Texture* t = SDL_CreateTexture(r, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, vga.h_active_pixels, vga.v_active_lines); 66 | 67 | Verilated::commandArgs(argc, argv); 68 | TOP_MODULE *top = new TOP_MODULE; 69 | 70 | bool quit = false; // Main single frame loop 71 | for (int frame = 0; !quit && !Verilated::gotFinish(); frame++) { 72 | int last_ticks = SDL_GetTicks(); 73 | SDL_Event e; 74 | while (SDL_PollEvent(&e)) { 75 | if (e.type == SDL_QUIT) quit = true; 76 | else if (e.type == SDL_KEYDOWN) { 77 | switch (e.key.keysym.sym) { 78 | case SDLK_ESCAPE: 79 | case SDLK_q: 80 | quit = true; 81 | break; 82 | case SDLK_f: // toggle fullscreen window 83 | fullscreen = fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; 84 | SDL_SetWindowFullscreen(w, fullscreen); 85 | break; 86 | case SDLK_p: // toggle VGA sync polarity 87 | polarity = !polarity; 88 | break; 89 | case SDLK_s: // toggle slow 90 | slow = !slow; 91 | default: 92 | break; 93 | } 94 | } 95 | } 96 | 97 | auto k = SDL_GetKeyboardState(NULL); 98 | auto rst_n = k[SDL_SCANCODE_R]; 99 | static bool rst_init = false; 100 | if (!rst_init) { rst_n = rst_init = true; } // reset on first clock cycle 101 | #if 1 102 | uint8_t ui_in = 0; 103 | ui_in |= k[SDL_SCANCODE_0] << 0; 104 | ui_in |= k[SDL_SCANCODE_1] << 1; 105 | ui_in |= k[SDL_SCANCODE_2] << 2; 106 | ui_in |= k[SDL_SCANCODE_3] << 3; 107 | ui_in |= k[SDL_SCANCODE_4] << 4; 108 | ui_in |= k[SDL_SCANCODE_5] << 5; 109 | ui_in |= k[SDL_SCANCODE_6] << 6; 110 | ui_in |= k[SDL_SCANCODE_7] << 7; 111 | #else 112 | static uint8_t ui_in = 0; 113 | std::map schedule { 114 | { 0, 0b01110110}, // H 115 | { 30, 0b01111011}, // e 116 | { 60, 0b00111000}, // L 117 | { 80, 0b00000000}, // 118 | { 90, 0b00111000}, // L 119 | {120, 0b01011100}, // o 120 | {150, 0b00000000}, // 121 | {180, 0b01001111}, // W 122 | {210, 0b01011100}, // o 123 | {240, 0b01010000}, // r 124 | {270, 0b00111000}, // L 125 | {300, 0b01011110}, // d 126 | {330, 0b00000000}, // 127 | {360, 0b10000000}, // . 128 | {390, 0b00000000} // 129 | }; 130 | auto it = schedule.find(frame); 131 | if (it != schedule.end()) ui_in = it->second; 132 | #endif 133 | static int hnum = 0; 134 | static int vnum = 0; 135 | for (int cycle = 0; cycle < vga.frame_cycles(); cycle++) { // Intra-frame verilator cycles 136 | // set inputs and tick-tock 137 | top->clk = 0; 138 | top->eval(); 139 | if (rst_n) top->rst_n = 0; 140 | top->ui_in = ui_in; 141 | top->clk = 1; 142 | top->eval(); 143 | if (rst_n) top->rst_n = 1; 144 | top->ui_in = ui_in; 145 | 146 | VGApinout_t uo_out{top->uo_out}; 147 | 148 | // h and v blank/sync logic 149 | if ((uo_out.hsync == vga.h_sync_pol) ^ polarity && (uo_out.vsync == vga.v_sync_pol) ^ polarity) { 150 | hnum = -vga.h_back_porch; 151 | vnum = -vga.v_back_porch; 152 | } 153 | 154 | // active frame, scaling for 6-bit color 155 | if ((hnum >= 0) && (hnum < vga.h_active_pixels) && (vnum >= 0) && (vnum < vga.v_active_lines)) { 156 | uint8_t rr = 85 * (uo_out.r1 << 1 | uo_out.r0); 157 | uint8_t gg = 85 * (uo_out.g1 << 1 | uo_out.g0); 158 | uint8_t bb = 85 * (uo_out.b1 << 1 | uo_out.b0); 159 | ABGR8888_t rgb = { .r = rr, .g = gg, .b = bb }; 160 | fb[vnum * vga.h_active_pixels + hnum] = rgb; 161 | } 162 | 163 | // keep track of encountered fields 164 | hnum++; 165 | if (hnum >= vga.h_active_pixels + vga.h_front_porch + vga.h_sync_pulse) { 166 | hnum = -vga.h_back_porch; 167 | vnum++; 168 | } 169 | } 170 | 171 | SDL_RenderClear(r); 172 | SDL_UpdateTexture(t, NULL, fb.data(), vga.h_active_pixels * sizeof(ABGR8888_t)); 173 | SDL_RenderCopy(r, t, NULL, NULL); 174 | SDL_RenderPresent(r); 175 | 176 | int ticks = SDL_GetTicks(); 177 | static int last_update_ticks = 0; 178 | if (ticks - last_update_ticks > 500) { 179 | last_update_ticks = ticks; 180 | std::string fps = "Tiny Tapeout VGA (" + std::to_string((int)1000.0/(ticks - last_ticks)) + " FPS)"; 181 | SDL_SetWindowTitle(w, fps.c_str()); 182 | } 183 | if (gif) { 184 | GifWriteFrame(&g, (uint8_t*)fb.data(), vga.h_active_pixels, vga.v_active_lines, delay); 185 | if (frame == gif_frames) quit = true; 186 | } 187 | if (slow) usleep(250000); // ~4 fps 188 | 189 | } 190 | 191 | if (gif) GifEnd(&g); 192 | 193 | top->final(); 194 | delete top; 195 | 196 | SDL_DestroyRenderer(r); 197 | SDL_DestroyWindow(w); 198 | SDL_Quit(); 199 | } 200 | -------------------------------------------------------------------------------- /gif.h: -------------------------------------------------------------------------------- 1 | // 2 | // gif.h 3 | // by Charlie Tangora 4 | // Public domain. 5 | // Email me : ctangora -at- gmail -dot- com 6 | // 7 | // This file offers a simple, very limited way to create animated GIFs directly in code. 8 | // 9 | // Those looking for particular cleverness are likely to be disappointed; it's pretty 10 | // much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg 11 | // dithering. (It does at least use delta encoding - only the changed portions of each 12 | // frame are saved.) 13 | // 14 | // So resulting files are often quite large. The hope is that it will be handy nonetheless 15 | // as a quick and easily-integrated way for programs to spit out animations. 16 | // 17 | // Only RGBA8 is currently supported as an input format. (The alpha is ignored.) 18 | // 19 | // If capturing a buffer with a bottom-left origin (such as OpenGL), define GIF_FLIP_VERT 20 | // to automatically flip the buffer data when writing the image (the buffer itself is 21 | // unchanged. 22 | // 23 | // USAGE: 24 | // Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. 25 | // Pass subsequent frames to GifWriteFrame(). 26 | // Finally, call GifEnd() to close the file handle and free memory. 27 | // 28 | 29 | #ifndef gif_h 30 | #define gif_h 31 | 32 | #include // for FILE* 33 | #include // for memcpy and bzero 34 | #include // for integer typedefs 35 | #include // for bool macros 36 | 37 | // Define these macros to hook into a custom memory allocator. 38 | // TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs 39 | // and any temp memory allocated by a function will be freed before it exits. 40 | // MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which 41 | // is used to find changed pixels for delta-encoding.) 42 | 43 | #ifndef GIF_TEMP_MALLOC 44 | #include 45 | #define GIF_TEMP_MALLOC malloc 46 | #endif 47 | 48 | #ifndef GIF_TEMP_FREE 49 | #include 50 | #define GIF_TEMP_FREE free 51 | #endif 52 | 53 | #ifndef GIF_MALLOC 54 | #include 55 | #define GIF_MALLOC malloc 56 | #endif 57 | 58 | #ifndef GIF_FREE 59 | #include 60 | #define GIF_FREE free 61 | #endif 62 | 63 | const int kGifTransIndex = 0; 64 | 65 | typedef struct 66 | { 67 | int bitDepth; 68 | 69 | uint8_t r[256]; 70 | uint8_t g[256]; 71 | uint8_t b[256]; 72 | 73 | // k-d tree over RGB space, organized in heap fashion 74 | // i.e. left child of node i is node i*2, right child is node i*2+1 75 | // nodes 256-511 are implicitly the leaves, containing a color 76 | uint8_t treeSplitElt[256]; 77 | uint8_t treeSplit[256]; 78 | } GifPalette; 79 | 80 | // max, min, and abs functions 81 | int GifIMax(int l, int r) { return l>r?l:r; } 82 | int GifIMin(int l, int r) { return l (1<bitDepth)-1) 93 | { 94 | int ind = treeRoot-(1<bitDepth); 95 | if(ind == kGifTransIndex) return; 96 | 97 | // check whether this color is better than the current winner 98 | int r_err = r - ((int32_t)pPal->r[ind]); 99 | int g_err = g - ((int32_t)pPal->g[ind]); 100 | int b_err = b - ((int32_t)pPal->b[ind]); 101 | int diff = GifIAbs(r_err)+GifIAbs(g_err)+GifIAbs(b_err); 102 | 103 | if(diff < *bestDiff) 104 | { 105 | *bestInd = ind; 106 | *bestDiff = diff; 107 | } 108 | 109 | return; 110 | } 111 | 112 | // take the appropriate color (r, g, or b) for this node of the k-d tree 113 | int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; 114 | int splitComp = comps[pPal->treeSplitElt[treeRoot]]; 115 | 116 | int splitPos = pPal->treeSplit[treeRoot]; 117 | if(splitPos > splitComp) 118 | { 119 | // check the left subtree 120 | GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2); 121 | if( *bestDiff > splitPos - splitComp ) 122 | { 123 | // cannot prove there's not a better value in the right subtree, check that too 124 | GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2+1); 125 | } 126 | } 127 | else 128 | { 129 | GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2+1); 130 | if( *bestDiff > splitComp - splitPos ) 131 | { 132 | GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2); 133 | } 134 | } 135 | } 136 | 137 | void GifSwapPixels(uint8_t* image, int pixA, int pixB) 138 | { 139 | uint8_t rA = image[pixA*4]; 140 | uint8_t gA = image[pixA*4+1]; 141 | uint8_t bA = image[pixA*4+2]; 142 | uint8_t aA = image[pixA*4+3]; 143 | 144 | uint8_t rB = image[pixB*4]; 145 | uint8_t gB = image[pixB*4+1]; 146 | uint8_t bB = image[pixB*4+2]; 147 | uint8_t aB = image[pixA*4+3]; 148 | 149 | image[pixA*4] = rB; 150 | image[pixA*4+1] = gB; 151 | image[pixA*4+2] = bB; 152 | image[pixA*4+3] = aB; 153 | 154 | image[pixB*4] = rA; 155 | image[pixB*4+1] = gA; 156 | image[pixB*4+2] = bA; 157 | image[pixB*4+3] = aA; 158 | } 159 | 160 | // just the partition operation from quicksort 161 | int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotValue) 162 | { 163 | int storeIndex = left; 164 | bool split = 0; 165 | for(int ii=left; ii neededCenter) 198 | GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); 199 | 200 | if(pivotIndex < neededCenter) 201 | GifPartitionByMedian(image, pivotIndex+1, right, com, neededCenter); 202 | } 203 | } 204 | 205 | // Just partition around a given pivot, returning the split point 206 | int GifPartitionByMean(uint8_t* image, int left, int right, int com, int neededMean) 207 | { 208 | if(left < right-1) 209 | { 210 | return GifPartition(image, left, right-1, com, neededMean); 211 | } 212 | return left; 213 | } 214 | 215 | // Builds a palette by creating a balanced k-d tree of all pixels in the image 216 | void GifSplitPalette(uint8_t* image, int numPixels, int treeNode, int treeLevel, bool buildForDither, GifPalette* pal) 217 | { 218 | if(numPixels == 0) 219 | return; 220 | 221 | int numColors = (1 << pal->bitDepth); 222 | 223 | // base case, bottom of the tree 224 | if(treeNode >= numColors) 225 | { 226 | int entry = treeNode - numColors; 227 | 228 | if(buildForDither) 229 | { 230 | // Dithering needs at least one color as dark as anything 231 | // in the image and at least one brightest color - 232 | // otherwise it builds up error and produces strange artifacts 233 | if( entry == 1 ) 234 | { 235 | // special case: the darkest color in the image 236 | uint32_t r=255, g=255, b=255; 237 | for(int ii=0; iir[entry] = (uint8_t)r; 245 | pal->g[entry] = (uint8_t)g; 246 | pal->b[entry] = (uint8_t)b; 247 | 248 | return; 249 | } 250 | 251 | if( entry == numColors-1 ) 252 | { 253 | // special case: the lightest color in the image 254 | uint32_t r=0, g=0, b=0; 255 | for(int ii=0; iir[entry] = (uint8_t)r; 263 | pal->g[entry] = (uint8_t)g; 264 | pal->b[entry] = (uint8_t)b; 265 | 266 | return; 267 | } 268 | } 269 | 270 | // otherwise, take the average of all colors in this subcube 271 | uint64_t r=0, g=0, b=0; 272 | for(int ii=0; iir[entry] = (uint8_t)r; 288 | pal->g[entry] = (uint8_t)g; 289 | pal->b[entry] = (uint8_t)b; 290 | 291 | return; 292 | } 293 | 294 | // Find the axis with the largest range 295 | int minR = 255, maxR = 0; 296 | int minG = 255, maxG = 0; 297 | int minB = 255, maxB = 0; 298 | for(int ii=0; ii maxR) maxR = r; 305 | if(r < minR) minR = r; 306 | 307 | if(g > maxG) maxG = g; 308 | if(g < minG) minG = g; 309 | 310 | if(b > maxB) maxB = b; 311 | if(b < minB) minB = b; 312 | } 313 | 314 | int rRange = maxR - minR; 315 | int gRange = maxG - minG; 316 | int bRange = maxB - minB; 317 | 318 | // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) 319 | int splitCom = 1; int rangeMin = minG; int rangeMax = maxG; 320 | if(bRange > gRange) { splitCom = 2; rangeMin = minB; rangeMax = maxB; } 321 | if(rRange > bRange && rRange > gRange) { splitCom = 0; rangeMin = minR; rangeMax = maxR; } 322 | 323 | int subPixelsA = numPixels / 2; 324 | 325 | GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); 326 | int splitValue = image[subPixelsA*4+splitCom]; 327 | 328 | // if the split is very unbalanced, split at the mean instead of the median to preserve rare colors 329 | int splitUnbalance = GifIAbs( (splitValue - rangeMin) - (rangeMax - splitValue) ); 330 | if( splitUnbalance > (1536 >> treeLevel) ) 331 | { 332 | splitValue = rangeMin + (rangeMax-rangeMin) / 2; 333 | subPixelsA = GifPartitionByMean(image, 0, numPixels, splitCom, splitValue); 334 | } 335 | 336 | // add the bottom node for the transparency index 337 | if( treeNode == numColors/2 ) 338 | { 339 | subPixelsA = 0; 340 | splitValue = 0; 341 | } 342 | 343 | int subPixelsB = numPixels-subPixelsA; 344 | pal->treeSplitElt[treeNode] = (uint8_t)splitCom; 345 | pal->treeSplit[treeNode] = (uint8_t)splitValue; 346 | 347 | GifSplitPalette(image, subPixelsA, treeNode*2, treeLevel+1, buildForDither, pal); 348 | GifSplitPalette(image+subPixelsA*4, subPixelsB, treeNode*2+1, treeLevel+1, buildForDither, pal); 349 | } 350 | 351 | // Finds all pixels that have changed from the previous image and 352 | // moves them to the fromt of th buffer. 353 | // This allows us to build a palette optimized for the colors of the 354 | // changed pixels only. 355 | int GifPickChangedPixels( const uint8_t* lastFrame, uint8_t* frame, int numPixels ) 356 | { 357 | int numChanged = 0; 358 | uint8_t* writeIter = frame; 359 | 360 | for (int ii=0; iibitDepth = bitDepth; 384 | 385 | // SplitPalette is destructive (it sorts the pixels by color) so 386 | // we must create a copy of the image for it to destroy 387 | size_t imageSize = (size_t)(width * height * 4 * sizeof(uint8_t)); 388 | uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); 389 | memcpy(destroyableImage, nextFrame, imageSize); 390 | 391 | int numPixels = (int)(width * height); 392 | if(lastFrame) 393 | numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); 394 | 395 | GifSplitPalette(destroyableImage, numPixels, 1, 0, buildForDither, pPal); 396 | 397 | GIF_TEMP_FREE(destroyableImage); 398 | 399 | // add the bottom node for the transparency index 400 | pPal->treeSplit[1 << (bitDepth-1)] = 0; 401 | pPal->treeSplitElt[1 << (bitDepth-1)] = 0; 402 | 403 | pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; 404 | } 405 | 406 | // Implements Floyd-Steinberg dithering, writes palette value to alpha 407 | void GifDitherImage( const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal ) 408 | { 409 | int numPixels = (int)(width * height); 410 | 411 | // quantPixels initially holds color*256 for all pixels 412 | // The extra 8 bits of precision allow for sub-single-color error values 413 | // to be propagated 414 | int32_t *quantPixels = (int32_t *)GIF_TEMP_MALLOC(sizeof(int32_t) * (size_t)numPixels * 4); 415 | 416 | for( int ii=0; iir[bestInd]) * 256; 457 | int32_t g_err = nextPix[1] - (int32_t)(pPal->g[bestInd]) * 256; 458 | int32_t b_err = nextPix[2] - (int32_t)(pPal->b[bestInd]) * 256; 459 | 460 | nextPix[0] = pPal->r[bestInd]; 461 | nextPix[1] = pPal->g[bestInd]; 462 | nextPix[2] = pPal->b[bestInd]; 463 | nextPix[3] = bestInd; 464 | 465 | // Propagate the error to the four adjacent locations 466 | // that we haven't touched yet 467 | int quantloc_7 = (int)(yy * width + xx + 1); 468 | int quantloc_3 = (int)(yy * width + width + xx - 1); 469 | int quantloc_5 = (int)(yy * width + width + xx); 470 | int quantloc_1 = (int)(yy * width + width + xx + 1); 471 | 472 | if(quantloc_7 < numPixels) 473 | { 474 | int32_t* pix7 = quantPixels+4*quantloc_7; 475 | pix7[0] += GifIMax( -pix7[0], r_err * 7 / 16 ); 476 | pix7[1] += GifIMax( -pix7[1], g_err * 7 / 16 ); 477 | pix7[2] += GifIMax( -pix7[2], b_err * 7 / 16 ); 478 | } 479 | 480 | if(quantloc_3 < numPixels) 481 | { 482 | int32_t* pix3 = quantPixels+4*quantloc_3; 483 | pix3[0] += GifIMax( -pix3[0], r_err * 3 / 16 ); 484 | pix3[1] += GifIMax( -pix3[1], g_err * 3 / 16 ); 485 | pix3[2] += GifIMax( -pix3[2], b_err * 3 / 16 ); 486 | } 487 | 488 | if(quantloc_5 < numPixels) 489 | { 490 | int32_t* pix5 = quantPixels+4*quantloc_5; 491 | pix5[0] += GifIMax( -pix5[0], r_err * 5 / 16 ); 492 | pix5[1] += GifIMax( -pix5[1], g_err * 5 / 16 ); 493 | pix5[2] += GifIMax( -pix5[2], b_err * 5 / 16 ); 494 | } 495 | 496 | if(quantloc_1 < numPixels) 497 | { 498 | int32_t* pix1 = quantPixels+4*quantloc_1; 499 | pix1[0] += GifIMax( -pix1[0], r_err / 16 ); 500 | pix1[1] += GifIMax( -pix1[1], g_err / 16 ); 501 | pix1[2] += GifIMax( -pix1[2], b_err / 16 ); 502 | } 503 | } 504 | } 505 | 506 | // Copy the palettized result to the output buffer 507 | for( int ii=0; iir[bestInd]; 542 | outFrame[1] = pPal->g[bestInd]; 543 | outFrame[2] = pPal->b[bestInd]; 544 | outFrame[3] = (uint8_t)bestInd; 545 | } 546 | 547 | if(lastFrame) lastFrame += 4; 548 | outFrame += 4; 549 | nextFrame += 4; 550 | } 551 | } 552 | 553 | // Simple structure to write out the LZW-compressed portion of the image 554 | // one bit at a time 555 | typedef struct 556 | { 557 | uint32_t chunkIndex; 558 | uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file 559 | 560 | uint8_t bitIndex; // how many bits in the partial byte written so far 561 | uint8_t byte; // current partial byte 562 | 563 | uint8_t padding[2]; // make padding explicit 564 | } GifBitStatus; 565 | 566 | // insert a single bit 567 | void GifWriteBit( GifBitStatus* stat, uint32_t bit ) 568 | { 569 | bit = bit & 1; 570 | bit = bit << stat->bitIndex; 571 | stat->byte |= bit; 572 | 573 | ++stat->bitIndex; 574 | if( stat->bitIndex > 7 ) 575 | { 576 | // move the newly-finished byte to the chunk buffer 577 | stat->chunk[stat->chunkIndex++] = stat->byte; 578 | // and start a new byte 579 | stat->bitIndex = 0; 580 | stat->byte = 0; 581 | } 582 | } 583 | 584 | // write all bytes so far to the file 585 | void GifWriteChunk( FILE* f, GifBitStatus* stat ) 586 | { 587 | fputc((int)stat->chunkIndex, f); 588 | fwrite(stat->chunk, 1, stat->chunkIndex, f); 589 | 590 | stat->bitIndex = 0; 591 | stat->byte = 0; 592 | stat->chunkIndex = 0; 593 | } 594 | 595 | void GifWriteCode( FILE* f, GifBitStatus* stat, uint32_t code, uint32_t length ) 596 | { 597 | for( uint32_t ii=0; ii> 1; 601 | 602 | if( stat->chunkIndex == 255 ) 603 | { 604 | GifWriteChunk(f, stat); 605 | } 606 | } 607 | } 608 | 609 | // The LZW dictionary is a 256-ary tree constructed as the file is encoded, 610 | // this is one node 611 | typedef struct 612 | { 613 | uint16_t m_next[256]; 614 | } GifLzwNode; 615 | 616 | // write a 256-color (8-bit) image palette to the file 617 | void GifWritePalette( const GifPalette* pPal, FILE* f ) 618 | { 619 | fputc(0, f); // first color: transparency 620 | fputc(0, f); 621 | fputc(0, f); 622 | 623 | for(int ii=1; ii<(1 << pPal->bitDepth); ++ii) 624 | { 625 | uint32_t r = pPal->r[ii]; 626 | uint32_t g = pPal->g[ii]; 627 | uint32_t b = pPal->b[ii]; 628 | 629 | fputc((int)r, f); 630 | fputc((int)g, f); 631 | fputc((int)b, f); 632 | } 633 | } 634 | 635 | // write the image header, LZW-compress and write out the image 636 | void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) 637 | { 638 | // graphics control extension 639 | fputc(0x21, f); 640 | fputc(0xf9, f); 641 | fputc(0x04, f); 642 | fputc(0x05, f); // leave prev frame in place, this frame has transparency 643 | fputc(delay & 0xff, f); 644 | fputc((delay >> 8) & 0xff, f); 645 | fputc(kGifTransIndex, f); // transparent color index 646 | fputc(0, f); 647 | 648 | fputc(0x2c, f); // image descriptor block 649 | 650 | fputc(left & 0xff, f); // corner of image in canvas space 651 | fputc((left >> 8) & 0xff, f); 652 | fputc(top & 0xff, f); 653 | fputc((top >> 8) & 0xff, f); 654 | 655 | fputc(width & 0xff, f); // width and height of image 656 | fputc((width >> 8) & 0xff, f); 657 | fputc(height & 0xff, f); 658 | fputc((height >> 8) & 0xff, f); 659 | 660 | //fputc(0, f); // no local color table, no transparency 661 | //fputc(0x80, f); // no local color table, but transparency 662 | 663 | fputc(0x80 + pPal->bitDepth-1, f); // local color table present, 2 ^ bitDepth entries 664 | GifWritePalette(pPal, f); 665 | 666 | const int minCodeSize = pPal->bitDepth; 667 | const uint32_t clearCode = 1 << pPal->bitDepth; 668 | 669 | fputc(minCodeSize, f); // min code size 8 bits 670 | 671 | GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode)*4096); 672 | 673 | memset(codetree, 0, sizeof(GifLzwNode)*4096); 674 | int32_t curCode = -1; 675 | uint32_t codeSize = (uint32_t)minCodeSize + 1; 676 | uint32_t maxCode = clearCode+1; 677 | 678 | GifBitStatus stat; 679 | stat.byte = 0; 680 | stat.bitIndex = 0; 681 | stat.chunkIndex = 0; 682 | 683 | GifWriteCode(f, &stat, clearCode, codeSize); // start with a fresh LZW dictionary 684 | 685 | for(uint32_t yy=0; yy= (1ul << codeSize) ) 720 | { 721 | // dictionary entry count has broken a size barrier, 722 | // we need more bits for codes 723 | codeSize++; 724 | } 725 | if( maxCode == 4095 ) 726 | { 727 | // the dictionary is full, clear it out and begin anew 728 | GifWriteCode(f, &stat, clearCode, codeSize); // clear tree 729 | 730 | memset(codetree, 0, sizeof(GifLzwNode)*4096); 731 | codeSize = (uint32_t)(minCodeSize + 1); 732 | maxCode = clearCode+1; 733 | } 734 | 735 | curCode = nextValue; 736 | } 737 | } 738 | } 739 | 740 | // compression footer 741 | GifWriteCode(f, &stat, (uint32_t)curCode, codeSize); 742 | GifWriteCode(f, &stat, clearCode, codeSize); 743 | GifWriteCode(f, &stat, clearCode + 1, (uint32_t)minCodeSize + 1); 744 | 745 | // write out the last partial chunk 746 | while( stat.bitIndex ) GifWriteBit(&stat, 0); 747 | if( stat.chunkIndex ) GifWriteChunk(f, &stat); 748 | 749 | fputc(0, f); // image block terminator 750 | 751 | GIF_TEMP_FREE(codetree); 752 | } 753 | 754 | typedef struct 755 | { 756 | FILE* f; 757 | uint8_t* oldImage; 758 | bool firstFrame; 759 | 760 | uint8_t padding[7]; // make padding explicit 761 | } GifWriter; 762 | 763 | // Creates a gif file. 764 | // The input GIFWriter is assumed to be uninitialized. 765 | // The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. 766 | bool GifBegin( GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false ) 767 | { 768 | (void)bitDepth; (void)dither; // Mute "Unused argument" warnings 769 | #if defined(_MSC_VER) && (_MSC_VER >= 1400) 770 | writer->f = 0; 771 | fopen_s(&writer->f, filename, "wb"); 772 | #else 773 | writer->f = fopen(filename, "wb"); 774 | #endif 775 | if(!writer->f) return false; 776 | 777 | writer->firstFrame = true; 778 | 779 | // allocate 780 | writer->oldImage = (uint8_t*)GIF_MALLOC(width*height*4); 781 | 782 | fputs("GIF89a", writer->f); 783 | 784 | // screen descriptor 785 | fputc(width & 0xff, writer->f); 786 | fputc((width >> 8) & 0xff, writer->f); 787 | fputc(height & 0xff, writer->f); 788 | fputc((height >> 8) & 0xff, writer->f); 789 | 790 | fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries 791 | fputc(0, writer->f); // background color 792 | fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) 793 | 794 | // now the "global" palette (really just a dummy palette) 795 | // color 0: black 796 | fputc(0, writer->f); 797 | fputc(0, writer->f); 798 | fputc(0, writer->f); 799 | // color 1: also black 800 | fputc(0, writer->f); 801 | fputc(0, writer->f); 802 | fputc(0, writer->f); 803 | 804 | if( delay != 0 ) 805 | { 806 | // animation header 807 | fputc(0x21, writer->f); // extension 808 | fputc(0xff, writer->f); // application specific 809 | fputc(11, writer->f); // length 11 810 | fputs("NETSCAPE2.0", writer->f); // yes, really 811 | fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data 812 | 813 | fputc(1, writer->f); // this is the Netscape 2.0 sub-block ID and it must be 1, otherwise some viewers error 814 | fputc(0, writer->f); // loop infinitely (byte 0) 815 | fputc(0, writer->f); // loop infinitely (byte 1) 816 | 817 | fputc(0, writer->f); // block terminator 818 | } 819 | 820 | return true; 821 | } 822 | 823 | // Writes out a new frame to a GIF in progress. 824 | // The GIFWriter should have been created by GIFBegin. 825 | // AFAIK, it is legal to use different bit depths for different frames of an image - 826 | // this may be handy to save bits in animations that don't change much. 827 | bool GifWriteFrame( GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, int bitDepth = 8, bool dither = false ) 828 | { 829 | if(!writer->f) return false; 830 | 831 | const uint8_t* oldImage = writer->firstFrame? NULL : writer->oldImage; 832 | writer->firstFrame = false; 833 | 834 | GifPalette pal; 835 | GifMakePalette((dither? NULL : oldImage), image, width, height, bitDepth, dither, &pal); 836 | 837 | if(dither) 838 | GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); 839 | else 840 | GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); 841 | 842 | GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); 843 | 844 | return true; 845 | } 846 | 847 | // Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. 848 | // Many if not most viewers will still display a GIF properly if the EOF code is missing, 849 | // but it's still a good idea to write it out. 850 | bool GifEnd( GifWriter* writer ) 851 | { 852 | if(!writer->f) return false; 853 | 854 | fputc(0x3b, writer->f); // end of file 855 | fclose(writer->f); 856 | GIF_FREE(writer->oldImage); 857 | 858 | writer->f = NULL; 859 | writer->oldImage = NULL; 860 | 861 | return true; 862 | } 863 | 864 | #endif 865 | --------------------------------------------------------------------------------