├── .gitignore ├── Makefile ├── README.md ├── qoi.h ├── qoi_verilator_shim.c ├── qoibench.c ├── qoiconv.c ├── qoifuzz.c └── verilog ├── qoi_decoder.v └── qoi_encoder.v /.gitignore: -------------------------------------------------------------------------------- 1 | images/ 2 | stb_image.h 3 | stb_image_write.h 4 | qoibench 5 | qoiconv 6 | 7 | *.o 8 | build/ 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS += -I/usr/include/stb/ -Wall -Wno-unused-function 2 | ifdef DEBUG 3 | CFLAGS += -DDEBUG 4 | endif 5 | 6 | .PHONY: all 7 | all: qoiconv qoibench 8 | 9 | TEST?=images/testcard 10 | 11 | .PHONY: test_encode 12 | test_encode: qoiconv 13 | ./qoiconv ${TEST}.png /tmp/ours.qoi 14 | md5sum /tmp/ours.qoi ${TEST}.qoi 15 | qoiconv ${TEST}.qoi /tmp/theirs.png 16 | qoiconv /tmp/ours.qoi /tmp/ours.png 17 | md5sum /tmp/ours.png /tmp/theirs.png 18 | 19 | .PHONY: test_decode 20 | test_decode: qoiconv 21 | ./qoiconv ${TEST}.qoi /tmp/ours.png 22 | qoiconv ${TEST}.qoi /tmp/theirs.png 23 | md5sum /tmp/ours.png /tmp/theirs.png 24 | 25 | .PHONY: fuzz 26 | fuzz: qoifuzz.c 27 | clang -fsanitize=address,fuzzer -g -O0 qoifuzz.c 28 | ./a.out; rm -Rf a.out 29 | 30 | .c.o: 31 | gcc $(CFLAGS) $< -c -o $@ 32 | 33 | qoibench: qoibench.o 34 | g++ -lpng ${LDFLAGS} qoibench.o -o qoibench 35 | 36 | qoiconv: qoiconv.o 37 | g++ ${LDFLAGS} qoiconv.o -o qoiconv 38 | 39 | qoiconv.o qoibench.o fuzz: qoi.h 40 | 41 | .PHONY: clean 42 | clean: 43 | rm -Rf *.o 44 | rm -Rf qoiconv qoibench a.out 45 | rm -Rf build/ 46 | 47 | ## Verilog via Verilator 48 | 49 | VFLAGS ?= -Wall 50 | 51 | # These are inane, something as simple as (var == 25) ? : will warn with this on 52 | VFLAGS += -Wno-WIDTH 53 | 54 | build/V%__ALL.o build/V%.h: verilog/%.v 55 | verilator $(VFLAGS) -cc $< --Mdir build/ --build 56 | 57 | build/verilated%o: /usr/share/verilator/include/verilated%cpp 58 | g++ $^ -c -o $@ 59 | 60 | build/qoi_verilator_shim.o: qoi_verilator_shim.c build/Vqoi_encoder.h build/Vqoi_decoder.h 61 | g++ -I /usr/share/verilator/include -I build ${CFLAGS} $< -c -o $@ 62 | 63 | FPGA_DEPS = build/Vqoi_encoder__ALL.o build/Vqoi_decoder__ALL.o 64 | FPGA_DEPS += build/verilated.o build/verilated_threads.o 65 | FPGA_DEPS += build/qoi_verilator_shim.o 66 | 67 | ifdef VERILATED 68 | CFLAGS += -DQOI_FPGA_IMPLEMENTATION 69 | qoiconv qoibench fuzz: $(FPGA_DEPS) 70 | LDFLAGS += $(FPGA_DEPS) 71 | endif 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![QOI Logo](https://qoiformat.org/qoi-logo.svg) 2 | 3 | QOI - The “Quite OK Image Format” for fast, lossless image compression. 4 | 5 | More info at https://qoiformat.org. 6 | 7 | # QOI targeted to FPGAs 8 | 9 | ## Verilog 10 | 11 | Fully featured encoder and decoder, all QOI_OPs supported according to the spec. 12 | 13 | ### Testing 14 | 15 | Verilator is able to emulate the fpga logic on a computer and is embeddable in C, 16 | enough for it to be a drop in qoi_{encode,decode} replacement for some of the 17 | reference implementation. 18 | 19 | Run `make VERILATED=1 test_encode`, it will convert an image to qoi and then use the 20 | system converter to convert back (both ours and the reference .qoi). The 21 | md5sums of the resulting png files should match (it should mean all pixels 22 | survived being encoded), the md5sums of the .qoi files should also match if 23 | it's a full featured encoder. 24 | 25 | Run `make VERILATED=1 test_decode` for a similar test exercising the decoding. 26 | 27 | ## LiteX 28 | 29 | Coming soon! 30 | 31 | ## Connections 32 | 33 | ### Encoder 34 | 35 | #### Input 36 | 37 | The `rgba` pixel data with a `clk`. 38 | 39 | If image is in 3 channel mode, an alpha of `0xff` must still be provided as 40 | encoder is always in 4 channel mode. Though if the alpha does not change from 41 | `0xff` as it's encoding, it will produce only 3 channel QOI_OPs. 42 | 43 | On the next clock after the last pixel please send a high `finish` so the 44 | encoder can immediatelly commit any QOI_OP_RUN that are in progress. 45 | 46 | #### Output 47 | 48 | The encoder will usually output a whole QOI `chunk` (1-5 bytes), though delayed 49 | by one clock cycle (to account for any QOI_OP_RUN). If Alpha is not needed 50 | the max chunk size is 4 bytes, which might fit in a single memory transaction. 51 | 52 | `chunk_len` specifies how many bytes the current `chunk` contains, could be 53 | 0 bytes in case of QOI_OP_RUN for many pixels, could be as high as 5 when 54 | we have a fresh RGBA pixel. 55 | 56 | This will eventually need to connect to some kind of bus like AXI or Avalon. 57 | Not really sure how to deal with the variable length output from the encoder yet. 58 | 59 | ### Decoder 60 | 61 | #### Input 62 | A way to peek at the next `chunk` (5 bytes if RGBA, or 4 for just RGB). 63 | 64 | For every clock the decoder will output how many bytes it "ate" via 65 | `chunk_len_consumed`, 66 | 67 | This value can be used to advance the peek pointer (or sometimes even stay 68 | in the same spot for a while in case of QOI_OP_RUN). 69 | 70 | #### Output 71 | 72 | One RGBA pixel per clock. 73 | -------------------------------------------------------------------------------- /qoi.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | 7 | QOI - The "Quite OK Image" format for fast, lossless image compression 8 | 9 | -- About 10 | 11 | QOI encodes and decodes images in a lossless format. Compared to stb_image and 12 | stb_image_write QOI offers 20x-50x faster encoding, 3x-4x faster decoding and 13 | 20% better compression. 14 | 15 | 16 | -- Synopsis 17 | 18 | // Define `QOI_IMPLEMENTATION` in *one* C/C++ file before including this 19 | // library to create the implementation. 20 | 21 | #define QOI_IMPLEMENTATION 22 | #include "qoi.h" 23 | 24 | // Encode and store an RGBA buffer to the file system. The qoi_desc describes 25 | // the input pixel data. 26 | qoi_write("image_new.qoi", rgba_pixels, &(qoi_desc){ 27 | .width = 1920, 28 | .height = 1080, 29 | .channels = 4, 30 | .colorspace = QOI_SRGB 31 | }); 32 | 33 | // Load and decode a QOI image from the file system into a 32bbp RGBA buffer. 34 | // The qoi_desc struct will be filled with the width, height, number of channels 35 | // and colorspace read from the file header. 36 | qoi_desc desc; 37 | void *rgba_pixels = qoi_read("image.qoi", &desc, 4); 38 | 39 | 40 | 41 | -- Documentation 42 | 43 | This library provides the following functions; 44 | - qoi_read -- read and decode a QOI file 45 | - qoi_decode -- decode the raw bytes of a QOI image from memory 46 | - qoi_write -- encode and write a QOI file 47 | - qoi_encode -- encode an rgba buffer into a QOI image in memory 48 | 49 | See the function declaration below for the signature and more information. 50 | 51 | If you don't want/need the qoi_read and qoi_write functions, you can define 52 | QOI_NO_STDIO before including this library. 53 | 54 | This library uses malloc() and free(). To supply your own malloc implementation 55 | you can define QOI_MALLOC and QOI_FREE before including this library. 56 | 57 | This library uses memset() to zero-initialize the index. To supply your own 58 | implementation you can define QOI_ZEROARR before including this library. 59 | 60 | 61 | -- Data Format 62 | 63 | A QOI file has a 14 byte header, followed by any number of data "chunks" and an 64 | 8-byte end marker. 65 | 66 | struct qoi_header_t { 67 | char magic[4]; // magic bytes "qoif" 68 | uint32_t width; // image width in pixels (BE) 69 | uint32_t height; // image height in pixels (BE) 70 | uint8_t channels; // 3 = RGB, 4 = RGBA 71 | uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear 72 | }; 73 | 74 | Images are encoded row by row, left to right, top to bottom. The decoder and 75 | encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous pixel value. An 76 | image is complete when all pixels specified by width * height have been covered. 77 | 78 | Pixels are encoded as 79 | - a run of the previous pixel 80 | - an index into an array of previously seen pixels 81 | - a difference to the previous pixel value in r,g,b 82 | - full r,g,b or r,g,b,a values 83 | 84 | The color channels are assumed to not be premultiplied with the alpha channel 85 | ("un-premultiplied alpha"). 86 | 87 | A running array[64] (zero-initialized) of previously seen pixel values is 88 | maintained by the encoder and decoder. Each pixel that is seen by the encoder 89 | and decoder is put into this array at the position formed by a hash function of 90 | the color value. In the encoder, if the pixel value at the index matches the 91 | current pixel, this index position is written to the stream as QOI_OP_INDEX. 92 | The hash function for the index is: 93 | 94 | index_position = (r * 3 + g * 5 + b * 7 + a * 11) % 64 95 | 96 | Each chunk starts with a 2- or 8-bit tag, followed by a number of data bits. The 97 | bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned. All 98 | values encoded in these data bits have the most significant bit on the left. 99 | 100 | The 8-bit tags have precedence over the 2-bit tags. A decoder must check for the 101 | presence of an 8-bit tag first. 102 | 103 | The byte stream's end is marked with 7 0x00 bytes followed a single 0x01 byte. 104 | 105 | 106 | The possible chunks are: 107 | 108 | 109 | .- QOI_OP_INDEX ----------. 110 | | Byte[0] | 111 | | 7 6 5 4 3 2 1 0 | 112 | |-------+-----------------| 113 | | 0 0 | index | 114 | `-------------------------` 115 | 2-bit tag b00 116 | 6-bit index into the color index array: 0..63 117 | 118 | A valid encoder must not issue 2 or more consecutive QOI_OP_INDEX chunks to the 119 | same index. QOI_OP_RUN should be used instead. 120 | 121 | 122 | .- QOI_OP_DIFF -----------. 123 | | Byte[0] | 124 | | 7 6 5 4 3 2 1 0 | 125 | |-------+-----+-----+-----| 126 | | 0 1 | dr | dg | db | 127 | `-------------------------` 128 | 2-bit tag b01 129 | 2-bit red channel difference from the previous pixel between -2..1 130 | 2-bit green channel difference from the previous pixel between -2..1 131 | 2-bit blue channel difference from the previous pixel between -2..1 132 | 133 | The difference to the current channel values are using a wraparound operation, 134 | so "1 - 2" will result in 255, while "255 + 1" will result in 0. 135 | 136 | Values are stored as unsigned integers with a bias of 2. E.g. -2 is stored as 137 | 0 (b00). 1 is stored as 3 (b11). 138 | 139 | The alpha value remains unchanged from the previous pixel. 140 | 141 | 142 | .- QOI_OP_LUMA -------------------------------------. 143 | | Byte[0] | Byte[1] | 144 | | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | 145 | |-------+-----------------+-------------+-----------| 146 | | 1 0 | green diff | dr - dg | db - dg | 147 | `---------------------------------------------------` 148 | 2-bit tag b10 149 | 6-bit green channel difference from the previous pixel -32..31 150 | 4-bit red channel difference minus green channel difference -8..7 151 | 4-bit blue channel difference minus green channel difference -8..7 152 | 153 | The green channel is used to indicate the general direction of change and is 154 | encoded in 6 bits. The red and blue channels (dr and db) base their diffs off 155 | of the green channel difference and are encoded in 4 bits. I.e.: 156 | dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g) 157 | db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g) 158 | 159 | The difference to the current channel values are using a wraparound operation, 160 | so "10 - 13" will result in 253, while "250 + 7" will result in 1. 161 | 162 | Values are stored as unsigned integers with a bias of 32 for the green channel 163 | and a bias of 8 for the red and blue channel. 164 | 165 | The alpha value remains unchanged from the previous pixel. 166 | 167 | 168 | .- QOI_OP_RUN ------------. 169 | | Byte[0] | 170 | | 7 6 5 4 3 2 1 0 | 171 | |-------+-----------------| 172 | | 1 1 | run | 173 | `-------------------------` 174 | 2-bit tag b11 175 | 6-bit run-length repeating the previous pixel: 1..62 176 | 177 | The run-length is stored with a bias of -1. Note that the run-lengths 63 and 64 178 | (b111110 and b111111) are illegal as they are occupied by the QOI_OP_RGB and 179 | QOI_OP_RGBA tags. 180 | 181 | 182 | .- QOI_OP_RGB ------------------------------------------. 183 | | Byte[0] | Byte[1] | Byte[2] | Byte[3] | 184 | | 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | 185 | |-------------------------+---------+---------+---------| 186 | | 1 1 1 1 1 1 1 0 | red | green | blue | 187 | `-------------------------------------------------------` 188 | 8-bit tag b11111110 189 | 8-bit red channel value 190 | 8-bit green channel value 191 | 8-bit blue channel value 192 | 193 | The alpha value remains unchanged from the previous pixel. 194 | 195 | 196 | .- QOI_OP_RGBA ---------------------------------------------------. 197 | | Byte[0] | Byte[1] | Byte[2] | Byte[3] | Byte[4] | 198 | | 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | 199 | |-------------------------+---------+---------+---------+---------| 200 | | 1 1 1 1 1 1 1 1 | red | green | blue | alpha | 201 | `-----------------------------------------------------------------` 202 | 8-bit tag b11111111 203 | 8-bit red channel value 204 | 8-bit green channel value 205 | 8-bit blue channel value 206 | 8-bit alpha channel value 207 | 208 | */ 209 | 210 | 211 | /* ----------------------------------------------------------------------------- 212 | Header - Public functions */ 213 | 214 | #ifndef QOI_H 215 | #define QOI_H 216 | 217 | #ifdef __cplusplus 218 | extern "C" { 219 | #endif 220 | 221 | /* A pointer to a qoi_desc struct has to be supplied to all of qoi's functions. 222 | It describes either the input format (for qoi_write and qoi_encode), or is 223 | filled with the description read from the file header (for qoi_read and 224 | qoi_decode). 225 | 226 | The colorspace in this qoi_desc is an enum where 227 | 0 = sRGB, i.e. gamma scaled RGB channels and a linear alpha channel 228 | 1 = all channels are linear 229 | You may use the constants QOI_SRGB or QOI_LINEAR. The colorspace is purely 230 | informative. It will be saved to the file header, but does not affect 231 | how chunks are en-/decoded. */ 232 | 233 | #define QOI_SRGB 0 234 | #define QOI_LINEAR 1 235 | 236 | typedef struct { 237 | unsigned int width; 238 | unsigned int height; 239 | unsigned char channels; 240 | unsigned char colorspace; 241 | } qoi_desc; 242 | 243 | #ifndef QOI_NO_STDIO 244 | 245 | /* Encode raw RGB or RGBA pixels into a QOI image and write it to the file 246 | system. The qoi_desc struct must be filled with the image width, height, 247 | number of channels (3 = RGB, 4 = RGBA) and the colorspace. 248 | 249 | The function returns 0 on failure (invalid parameters, or fopen or malloc 250 | failed) or the number of bytes written on success. */ 251 | 252 | int qoi_write(const char *filename, const void *data, const qoi_desc *desc); 253 | 254 | 255 | /* Read and decode a QOI image from the file system. If channels is 0, the 256 | number of channels from the file header is used. If channels is 3 or 4 the 257 | output format will be forced into this number of channels. 258 | 259 | The function either returns NULL on failure (invalid data, or malloc or fopen 260 | failed) or a pointer to the decoded pixels. On success, the qoi_desc struct 261 | will be filled with the description from the file header. 262 | 263 | The returned pixel data should be free()d after use. */ 264 | 265 | void *qoi_read(const char *filename, qoi_desc *desc, int channels); 266 | 267 | #endif /* QOI_NO_STDIO */ 268 | 269 | 270 | /* Encode raw RGB or RGBA pixels into a QOI image in memory. 271 | 272 | The function either returns NULL on failure (invalid parameters or malloc 273 | failed) or a pointer to the encoded data on success. On success the out_len 274 | is set to the size in bytes of the encoded data. 275 | 276 | The returned qoi data should be free()d after use. */ 277 | 278 | void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len); 279 | 280 | 281 | /* Decode a QOI image from memory. 282 | 283 | The function either returns NULL on failure (invalid parameters or malloc 284 | failed) or a pointer to the decoded pixels. On success, the qoi_desc struct 285 | is filled with the description from the file header. 286 | 287 | The returned pixel data should be free()d after use. */ 288 | 289 | void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels); 290 | 291 | 292 | #ifdef __cplusplus 293 | } 294 | #endif 295 | #endif /* QOI_H */ 296 | 297 | 298 | /* ----------------------------------------------------------------------------- 299 | Implementation */ 300 | 301 | #ifdef QOI_IMPLEMENTATION 302 | #include 303 | #include 304 | 305 | #ifndef QOI_MALLOC 306 | #define QOI_MALLOC(sz) malloc(sz) 307 | #define QOI_FREE(p) free(p) 308 | #endif 309 | #ifndef QOI_ZEROARR 310 | #define QOI_ZEROARR(a) memset((a),0,sizeof(a)) 311 | #endif 312 | 313 | #define QOI_OP_INDEX 0x00 /* 00xxxxxx */ 314 | #define QOI_OP_DIFF 0x40 /* 01xxxxxx */ 315 | #define QOI_OP_LUMA 0x80 /* 10xxxxxx */ 316 | #define QOI_OP_RUN 0xc0 /* 11xxxxxx */ 317 | #define QOI_OP_RGB 0xfe /* 11111110 */ 318 | #define QOI_OP_RGBA 0xff /* 11111111 */ 319 | 320 | #define QOI_MASK_2 0xc0 /* 11000000 */ 321 | 322 | #define QOI_COLOR_HASH(C) (C.rgba.r*3 + C.rgba.g*5 + C.rgba.b*7 + C.rgba.a*11) 323 | #define QOI_MAGIC \ 324 | (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | \ 325 | ((unsigned int)'i') << 8 | ((unsigned int)'f')) 326 | #define QOI_HEADER_SIZE 14 327 | 328 | /* 2GB is the max file size that this implementation can safely handle. We guard 329 | against anything larger than that, assuming the worst case with 5 bytes per 330 | pixel, rounded down to a nice clean value. 400 million pixels ought to be 331 | enough for anybody. */ 332 | #define QOI_PIXELS_MAX ((unsigned int)400000000) 333 | 334 | typedef union { 335 | struct { unsigned char r, g, b, a; } rgba; 336 | unsigned int v; 337 | } qoi_rgba_t; 338 | 339 | static const unsigned char qoi_padding[8] = {0,0,0,0,0,0,0,1}; 340 | 341 | static void qoi_write_32(unsigned char *bytes, int *p, unsigned int v) { 342 | bytes[(*p)++] = (0xff000000 & v) >> 24; 343 | bytes[(*p)++] = (0x00ff0000 & v) >> 16; 344 | bytes[(*p)++] = (0x0000ff00 & v) >> 8; 345 | bytes[(*p)++] = (0x000000ff & v); 346 | } 347 | 348 | static unsigned int qoi_read_32(const unsigned char *bytes, int *p) { 349 | unsigned int a = bytes[(*p)++]; 350 | unsigned int b = bytes[(*p)++]; 351 | unsigned int c = bytes[(*p)++]; 352 | unsigned int d = bytes[(*p)++]; 353 | return a << 24 | b << 16 | c << 8 | d; 354 | } 355 | 356 | #ifndef QOI_FPGA_IMPLEMENTATION 357 | void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { 358 | int i, max_size, p, run; 359 | int px_len, px_end, px_pos, channels; 360 | unsigned char *bytes; 361 | const unsigned char *pixels; 362 | qoi_rgba_t index[64]; 363 | qoi_rgba_t px, px_prev; 364 | 365 | if ( 366 | data == NULL || out_len == NULL || desc == NULL || 367 | desc->width == 0 || desc->height == 0 || 368 | desc->channels < 3 || desc->channels > 4 || 369 | desc->colorspace > 1 || 370 | desc->height >= QOI_PIXELS_MAX / desc->width 371 | ) { 372 | return NULL; 373 | } 374 | 375 | max_size = 376 | desc->width * desc->height * (desc->channels + 1) + 377 | QOI_HEADER_SIZE + sizeof(qoi_padding); 378 | 379 | p = 0; 380 | bytes = (unsigned char *) QOI_MALLOC(max_size); 381 | if (!bytes) { 382 | return NULL; 383 | } 384 | 385 | qoi_write_32(bytes, &p, QOI_MAGIC); 386 | qoi_write_32(bytes, &p, desc->width); 387 | qoi_write_32(bytes, &p, desc->height); 388 | bytes[p++] = desc->channels; 389 | bytes[p++] = desc->colorspace; 390 | 391 | 392 | pixels = (const unsigned char *)data; 393 | 394 | QOI_ZEROARR(index); 395 | 396 | run = 0; 397 | px_prev.rgba.r = 0; 398 | px_prev.rgba.g = 0; 399 | px_prev.rgba.b = 0; 400 | px_prev.rgba.a = 255; 401 | px = px_prev; 402 | 403 | px_len = desc->width * desc->height * desc->channels; 404 | px_end = px_len - desc->channels; 405 | channels = desc->channels; 406 | 407 | for (px_pos = 0; px_pos < px_len; px_pos += channels) { 408 | px.rgba.r = pixels[px_pos + 0]; 409 | px.rgba.g = pixels[px_pos + 1]; 410 | px.rgba.b = pixels[px_pos + 2]; 411 | 412 | if (channels == 4) { 413 | px.rgba.a = pixels[px_pos + 3]; 414 | } 415 | 416 | if (px.v == px_prev.v) { 417 | run++; 418 | if (run == 62 || px_pos == px_end) { 419 | bytes[p++] = QOI_OP_RUN | (run - 1); 420 | printf("RUN %d\n", (run-1)); 421 | run = 0; 422 | } 423 | } 424 | else { 425 | int index_pos; 426 | 427 | if (run > 0) { 428 | bytes[p++] = QOI_OP_RUN | (run - 1); 429 | printf("RUN %d\n", (run-1)); 430 | run = 0; 431 | } 432 | 433 | index_pos = QOI_COLOR_HASH(px) % 64; 434 | 435 | if (index[index_pos].v == px.v) { 436 | bytes[p++] = QOI_OP_INDEX | index_pos; 437 | printf("INDEX %d\n", index_pos); 438 | } 439 | else { 440 | index[index_pos] = px; 441 | 442 | if (px.rgba.a == px_prev.rgba.a) { 443 | signed char vr = px.rgba.r - px_prev.rgba.r; 444 | signed char vg = px.rgba.g - px_prev.rgba.g; 445 | signed char vb = px.rgba.b - px_prev.rgba.b; 446 | 447 | signed char vg_r = vr - vg; 448 | signed char vg_b = vb - vg; 449 | 450 | if ( 451 | vr > -3 && vr < 2 && 452 | vg > -3 && vg < 2 && 453 | vb > -3 && vb < 2 454 | ) { 455 | bytes[p++] = QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2); 456 | printf("DIFF %d %d %d\n", vr, vg, vb); 457 | } 458 | else if ( 459 | vg_r > -9 && vg_r < 8 && 460 | vg > -33 && vg < 32 && 461 | vg_b > -9 && vg_b < 8 462 | ) { 463 | bytes[p++] = QOI_OP_LUMA | (vg + 32); 464 | bytes[p++] = (vg_r + 8) << 4 | (vg_b + 8); 465 | printf("LUMA %d %d %d\n", vg, vg_r, vg_b); 466 | } 467 | else { 468 | bytes[p++] = QOI_OP_RGB; 469 | bytes[p++] = px.rgba.r; 470 | bytes[p++] = px.rgba.g; 471 | bytes[p++] = px.rgba.b; 472 | printf("RGB %02x %02x %02x\n", bytes[p-3], bytes[p-2], bytes[p-1]); 473 | } 474 | } 475 | else { 476 | bytes[p++] = QOI_OP_RGBA; 477 | bytes[p++] = px.rgba.r; 478 | bytes[p++] = px.rgba.g; 479 | bytes[p++] = px.rgba.b; 480 | bytes[p++] = px.rgba.a; 481 | printf("RGBA %02x %02x %02x %02x\n", bytes[p-4], bytes[p-3], bytes[p-2], bytes[p-1]); 482 | } 483 | } 484 | } 485 | px_prev = px; 486 | } 487 | 488 | for (i = 0; i < (int)sizeof(qoi_padding); i++) { 489 | bytes[p++] = qoi_padding[i]; 490 | } 491 | 492 | *out_len = p; 493 | return bytes; 494 | } 495 | 496 | void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { 497 | const unsigned char *bytes; 498 | unsigned int header_magic; 499 | unsigned char *pixels; 500 | qoi_rgba_t index[64]; 501 | qoi_rgba_t px; 502 | int px_len, chunks_len, px_pos; 503 | int p = 0, run = 0; 504 | 505 | if ( 506 | data == NULL || desc == NULL || 507 | (channels != 0 && channels != 3 && channels != 4) || 508 | size < QOI_HEADER_SIZE + (int)sizeof(qoi_padding) 509 | ) { 510 | return NULL; 511 | } 512 | 513 | bytes = (const unsigned char *)data; 514 | 515 | header_magic = qoi_read_32(bytes, &p); 516 | desc->width = qoi_read_32(bytes, &p); 517 | desc->height = qoi_read_32(bytes, &p); 518 | desc->channels = bytes[p++]; 519 | desc->colorspace = bytes[p++]; 520 | 521 | if ( 522 | desc->width == 0 || desc->height == 0 || 523 | desc->channels < 3 || desc->channels > 4 || 524 | desc->colorspace > 1 || 525 | header_magic != QOI_MAGIC || 526 | desc->height >= QOI_PIXELS_MAX / desc->width 527 | ) { 528 | return NULL; 529 | } 530 | 531 | if (channels == 0) { 532 | channels = desc->channels; 533 | } 534 | 535 | px_len = desc->width * desc->height * channels; 536 | pixels = (unsigned char *) QOI_MALLOC(px_len); 537 | if (!pixels) { 538 | return NULL; 539 | } 540 | 541 | QOI_ZEROARR(index); 542 | px.rgba.r = 0; 543 | px.rgba.g = 0; 544 | px.rgba.b = 0; 545 | px.rgba.a = 255; 546 | 547 | chunks_len = size - (int)sizeof(qoi_padding); 548 | for (px_pos = 0; px_pos < px_len; px_pos += channels) { 549 | if (run > 0) { 550 | run--; 551 | } 552 | else if (p < chunks_len) { 553 | int b1 = bytes[p++]; 554 | 555 | if (b1 == QOI_OP_RGB) { 556 | px.rgba.r = bytes[p++]; 557 | px.rgba.g = bytes[p++]; 558 | px.rgba.b = bytes[p++]; 559 | printf("RGB %02x %02x %02x ", bytes[p-3], bytes[p-2], bytes[p-1]); 560 | } 561 | else if (b1 == QOI_OP_RGBA) { 562 | px.rgba.r = bytes[p++]; 563 | px.rgba.g = bytes[p++]; 564 | px.rgba.b = bytes[p++]; 565 | px.rgba.a = bytes[p++]; 566 | printf("RGBA %02x %02x %02x %02x ", bytes[p-4], bytes[p-3], bytes[p-2], bytes[p-1]); 567 | } 568 | else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) { 569 | px = index[b1]; 570 | printf("INDEX %d ", b1); 571 | } 572 | else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) { 573 | px.rgba.r += ((b1 >> 4) & 0x03) - 2; 574 | px.rgba.g += ((b1 >> 2) & 0x03) - 2; 575 | px.rgba.b += ( b1 & 0x03) - 2; 576 | printf("DIFF %d %d %d ", ((b1 >> 4) & 0x03) - 2, ((b1 >> 2) & 0x03) - 2, (b1 & 0x03) - 2); 577 | } 578 | else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) { 579 | int b2 = bytes[p++]; 580 | int vg = (b1 & 0x3f) - 32; 581 | px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f); 582 | px.rgba.g += vg; 583 | px.rgba.b += vg - 8 + (b2 & 0x0f); 584 | printf("LUMA %d %d %d ", vg, vg - 8 + ((b2 >> 4) & 0x0f), vg - 8 + (b2 & 0x0f)); 585 | } 586 | else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) { 587 | run = (b1 & 0x3f); 588 | printf("RUN %d ", run); 589 | } 590 | 591 | index[QOI_COLOR_HASH(px) % 64] = px; 592 | } 593 | 594 | pixels[px_pos + 0] = px.rgba.r; 595 | pixels[px_pos + 1] = px.rgba.g; 596 | pixels[px_pos + 2] = px.rgba.b; 597 | 598 | if (channels == 4) { 599 | pixels[px_pos + 3] = px.rgba.a; 600 | } 601 | 602 | printf("px = %08x\n", px.v); 603 | } 604 | 605 | return pixels; 606 | } 607 | #endif /* !QOI_FPGA_IMPLEMENTATION */ 608 | 609 | #ifndef QOI_NO_STDIO 610 | #include 611 | 612 | int qoi_write(const char *filename, const void *data, const qoi_desc *desc) { 613 | FILE *f = fopen(filename, "wb"); 614 | int size; 615 | void *encoded; 616 | 617 | if (!f) { 618 | return 0; 619 | } 620 | 621 | encoded = qoi_encode(data, desc, &size); 622 | if (!encoded) { 623 | fclose(f); 624 | return 0; 625 | } 626 | 627 | fwrite(encoded, 1, size, f); 628 | fclose(f); 629 | 630 | QOI_FREE(encoded); 631 | return size; 632 | } 633 | 634 | void *qoi_read(const char *filename, qoi_desc *desc, int channels) { 635 | FILE *f = fopen(filename, "rb"); 636 | int size, bytes_read; 637 | void *pixels, *data; 638 | 639 | if (!f) { 640 | return NULL; 641 | } 642 | 643 | fseek(f, 0, SEEK_END); 644 | size = ftell(f); 645 | if (size <= 0) { 646 | fclose(f); 647 | return NULL; 648 | } 649 | fseek(f, 0, SEEK_SET); 650 | 651 | data = QOI_MALLOC(size); 652 | if (!data) { 653 | fclose(f); 654 | return NULL; 655 | } 656 | 657 | bytes_read = fread(data, 1, size, f); 658 | fclose(f); 659 | 660 | pixels = qoi_decode(data, bytes_read, desc, channels); 661 | QOI_FREE(data); 662 | return pixels; 663 | } 664 | 665 | #endif /* QOI_NO_STDIO */ 666 | #endif /* QOI_IMPLEMENTATION */ 667 | -------------------------------------------------------------------------------- /qoi_verilator_shim.c: -------------------------------------------------------------------------------- 1 | /* Copyright(c) 2021 Dominic Szablewski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files(the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions : 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. */ 18 | 19 | // Contains drop-in replacement of qoi_{encode,decode} that use the verilog 20 | // implementation via verilator. 21 | 22 | #define QOI_IMPLEMENTATION 23 | #define QOI_NO_STDIO // We just want the qoi aux internal headers 24 | #include "qoi.h" 25 | 26 | #include "verilated.h" 27 | #include "Vqoi_encoder.h" 28 | #include "Vqoi_decoder.h" 29 | 30 | #ifdef DEBUG 31 | #include 32 | #define qoi_debug(...) printf(__VA_ARGS__) 33 | #else 34 | #define qoi_debug(...) 35 | #endif 36 | 37 | #define QOI_CHUNK_MAX 5 38 | 39 | int fpga_encode_chunk(Vqoi_encoder *v, qoi_rgba_t px, unsigned char *bytes, int *p) { 40 | int i; 41 | 42 | v->eval(); 43 | v->r = px.rgba.r; 44 | v->g = px.rgba.g; 45 | v->b = px.rgba.b; 46 | v->a = px.rgba.a; 47 | v->eval(); 48 | v->clk = 0; 49 | v->eval(); 50 | v->clk = 1; 51 | v->eval(); 52 | 53 | qoi_debug("p=%d rgba=%08x %d ", *p, px.v, v->chunk_len); 54 | 55 | for (i = 0; i < v->chunk_len; i++) { 56 | bytes[(*p)++] = v->chunk[i]; 57 | qoi_debug("%02x", v->chunk[i]); 58 | } 59 | qoi_debug("|"); 60 | for (;i < QOI_CHUNK_MAX; i++) 61 | qoi_debug("%02x", v->chunk[i]); 62 | qoi_debug("\n"); 63 | 64 | return v->chunk_len; 65 | } 66 | 67 | void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { 68 | int i, max_size, p; 69 | int px_len, px_pos, channels; 70 | unsigned char *bytes; 71 | const unsigned char *pixels; 72 | qoi_rgba_t px; 73 | 74 | if ( 75 | data == NULL || out_len == NULL || desc == NULL || 76 | desc->width == 0 || desc->height == 0 || 77 | desc->channels < 3 || desc->channels > 4 || 78 | desc->colorspace > 1 || 79 | desc->height >= QOI_PIXELS_MAX / desc->width 80 | ) { 81 | return NULL; 82 | } 83 | 84 | max_size = 85 | desc->width * desc->height * (desc->channels + 1) + 86 | QOI_HEADER_SIZE + sizeof(qoi_padding); 87 | 88 | p = 0; 89 | bytes = (unsigned char *) QOI_MALLOC(max_size); 90 | if (!bytes) { 91 | return NULL; 92 | } 93 | 94 | qoi_write_32(bytes, &p, QOI_MAGIC); 95 | qoi_write_32(bytes, &p, desc->width); 96 | qoi_write_32(bytes, &p, desc->height); 97 | bytes[p++] = desc->channels; 98 | bytes[p++] = desc->colorspace; 99 | 100 | pixels = (const unsigned char *)data; 101 | 102 | Vqoi_encoder *v = new Vqoi_encoder; 103 | v->rst = 1; 104 | v->clk = 0; 105 | v->eval(); 106 | v->clk = 1; 107 | v->eval(); 108 | v->rst = 0; 109 | 110 | px_len = desc->width * desc->height * desc->channels; 111 | channels = desc->channels; 112 | 113 | for (px_pos = 0; px_pos < px_len; px_pos += channels) { 114 | if (channels == 4) { 115 | px = *(qoi_rgba_t *)(pixels + px_pos); 116 | } 117 | else { 118 | px.rgba.r = pixels[px_pos + 0]; 119 | px.rgba.g = pixels[px_pos + 1]; 120 | px.rgba.b = pixels[px_pos + 2]; 121 | px.rgba.a = 255; 122 | } 123 | 124 | fpga_encode_chunk(v, px, bytes, &p); 125 | } 126 | qoi_debug("last pixel ^^^\n"); 127 | 128 | // signal encoder that we're done 129 | v->finish = 1; 130 | // but keep reading while there's still chunks in there 131 | while (fpga_encode_chunk(v, px /* don't care */, bytes, &p)); 132 | 133 | for (i = 0; i < (int)sizeof(qoi_padding); i++) { 134 | bytes[p++] = qoi_padding[i]; 135 | } 136 | 137 | *out_len = p; 138 | return bytes; 139 | } 140 | 141 | int fpga_decode_chunk(Vqoi_decoder *v, const unsigned char chunk[QOI_CHUNK_MAX], unsigned char **pixel_bytes, unsigned char *pixel_end, int channels) { 142 | int run = 0; 143 | 144 | // Let the decoder peek at the next QOI_CHUNK_MAX chunks 145 | v->eval(); 146 | for (int i = 0; i < QOI_CHUNK_MAX; i++) 147 | v->chunk[i] = chunk[i]; 148 | v->eval(); 149 | 150 | do { 151 | // Ask decoder for a pixel 152 | v->clk = 0; 153 | v->eval(); 154 | v->clk = 1; 155 | v->eval(); 156 | 157 | // Make sure we don't overflow in the middle of a RUN 158 | if (*pixel_bytes == pixel_end) break; 159 | 160 | // Copy pixel to our buffer 161 | (*(*pixel_bytes)++) = v->r; 162 | (*(*pixel_bytes)++) = v->g; 163 | (*(*pixel_bytes)++) = v->b; 164 | if (channels == 4) 165 | (*(*pixel_bytes)++) = v->a; 166 | 167 | // As long as decoder is not bored of the current chunk and 168 | // is spewing out pixels. 169 | run++; 170 | } while(!v->chunk_len_consumed); 171 | 172 | // Some debugging 173 | for (int i = 0; i < QOI_CHUNK_MAX; i++) 174 | if (i < v->chunk_len_consumed) 175 | qoi_debug("%02x", chunk[i]); 176 | else 177 | qoi_debug(" "); 178 | qoi_debug(" => %02x%02x%02x%02x", v->r, v->g, v->b, v->a); 179 | if (run > 1) qoi_debug("*%d", run); 180 | qoi_debug("\n"); 181 | 182 | return v->chunk_len_consumed; 183 | } 184 | 185 | void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { 186 | const unsigned char *bytes; 187 | unsigned int header_magic; 188 | unsigned char *pixels; 189 | int px_len, chunks_len; 190 | int p = 0; 191 | 192 | if ( 193 | data == NULL || desc == NULL || 194 | (channels != 0 && channels != 3 && channels != 4) || 195 | size < QOI_HEADER_SIZE + (int)sizeof(qoi_padding) 196 | ) { 197 | return NULL; 198 | } 199 | 200 | bytes = (const unsigned char *)data; 201 | 202 | header_magic = qoi_read_32(bytes, &p); 203 | desc->width = qoi_read_32(bytes, &p); 204 | desc->height = qoi_read_32(bytes, &p); 205 | desc->channels = bytes[p++]; 206 | desc->colorspace = bytes[p++]; 207 | 208 | if ( 209 | desc->width == 0 || desc->height == 0 || 210 | desc->channels < 3 || desc->channels > 4 || 211 | desc->colorspace > 1 || 212 | header_magic != QOI_MAGIC || 213 | desc->height >= QOI_PIXELS_MAX / desc->width 214 | ) { 215 | return NULL; 216 | } 217 | 218 | if (channels == 0) { 219 | channels = desc->channels; 220 | } 221 | 222 | px_len = desc->width * desc->height * channels; 223 | pixels = (unsigned char *) QOI_MALLOC(px_len); 224 | if (!pixels) { 225 | return NULL; 226 | } 227 | 228 | chunks_len = size - (int)sizeof(qoi_padding); 229 | unsigned char *current_pixel = pixels; 230 | unsigned char *pixel_end = pixels + px_len; 231 | 232 | Vqoi_decoder *v = new Vqoi_decoder; 233 | v->rst = 1; 234 | v->clk = 0; 235 | v->eval(); 236 | v->clk = 1; 237 | v->eval(); 238 | v->rst = 0; 239 | 240 | while ((p < chunks_len) && (current_pixel < pixel_end)) { 241 | const unsigned char *current_chunk = bytes + p; 242 | 243 | qoi_debug("%d:", p); 244 | int chunk_len_consumed = 245 | fpga_decode_chunk(v, current_chunk, ¤t_pixel, pixel_end, channels); 246 | 247 | p += chunk_len_consumed; 248 | } 249 | 250 | return pixels; 251 | } 252 | -------------------------------------------------------------------------------- /qoibench.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | 7 | Simple benchmark suite for png, stbi and qoi 8 | 9 | Requires libpng, "stb_image.h" and "stb_image_write.h" 10 | Compile with: 11 | gcc qoibench.c -std=gnu99 -lpng -O3 -o qoibench 12 | 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #define STB_IMAGE_IMPLEMENTATION 20 | #define STBI_ONLY_PNG 21 | #define STBI_NO_LINEAR 22 | #include "stb_image.h" 23 | 24 | #define STB_IMAGE_WRITE_IMPLEMENTATION 25 | #include "stb_image_write.h" 26 | 27 | #define QOI_IMPLEMENTATION 28 | #include "qoi.h" 29 | 30 | 31 | 32 | 33 | // ----------------------------------------------------------------------------- 34 | // Cross platform high resolution timer 35 | // From https://gist.github.com/ForeverZer0/0a4f80fc02b96e19380ebb7a3debbee5 36 | 37 | #include 38 | #if defined(__linux) 39 | #define HAVE_POSIX_TIMER 40 | #include 41 | #ifdef CLOCK_MONOTONIC 42 | #define CLOCKID CLOCK_MONOTONIC 43 | #else 44 | #define CLOCKID CLOCK_REALTIME 45 | #endif 46 | #elif defined(__APPLE__) 47 | #define HAVE_MACH_TIMER 48 | #include 49 | #elif defined(_WIN32) 50 | #define WIN32_LEAN_AND_MEAN 51 | #include 52 | #endif 53 | 54 | static uint64_t ns() { 55 | static uint64_t is_init = 0; 56 | #if defined(__APPLE__) 57 | static mach_timebase_info_data_t info; 58 | if (0 == is_init) { 59 | mach_timebase_info(&info); 60 | is_init = 1; 61 | } 62 | uint64_t now; 63 | now = mach_absolute_time(); 64 | now *= info.numer; 65 | now /= info.denom; 66 | return now; 67 | #elif defined(__linux) 68 | static struct timespec linux_rate; 69 | if (0 == is_init) { 70 | clock_getres(CLOCKID, &linux_rate); 71 | is_init = 1; 72 | } 73 | uint64_t now; 74 | struct timespec spec; 75 | clock_gettime(CLOCKID, &spec); 76 | now = spec.tv_sec * 1.0e9 + spec.tv_nsec; 77 | return now; 78 | #elif defined(_WIN32) 79 | static LARGE_INTEGER win_frequency; 80 | if (0 == is_init) { 81 | QueryPerformanceFrequency(&win_frequency); 82 | is_init = 1; 83 | } 84 | LARGE_INTEGER now; 85 | QueryPerformanceCounter(&now); 86 | return (uint64_t) ((1e9 * now.QuadPart) / win_frequency.QuadPart); 87 | #endif 88 | } 89 | 90 | #define STRINGIFY(x) #x 91 | #define TOSTRING(x) STRINGIFY(x) 92 | #define ERROR(...) printf("abort at line " TOSTRING(__LINE__) ": " __VA_ARGS__); printf("\n"); exit(1) 93 | 94 | 95 | // ----------------------------------------------------------------------------- 96 | // libpng encode/decode wrappers 97 | // Seriously, who thought this was a good abstraction for an API to read/write 98 | // images? 99 | 100 | typedef struct { 101 | int size; 102 | int capacity; 103 | unsigned char *data; 104 | } libpng_write_t; 105 | 106 | void libpng_encode_callback(png_structp png_ptr, png_bytep data, png_size_t length) { 107 | libpng_write_t *write_data = (libpng_write_t*)png_get_io_ptr(png_ptr); 108 | if (write_data->size + length >= write_data->capacity) { 109 | ERROR("PNG write"); 110 | } 111 | memcpy(write_data->data + write_data->size, data, length); 112 | write_data->size += length; 113 | } 114 | 115 | void *libpng_encode(void *pixels, int w, int h, int channels, int *out_len) { 116 | png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 117 | if (!png) { 118 | ERROR("png_create_write_struct"); 119 | } 120 | 121 | png_infop info = png_create_info_struct(png); 122 | if (!info) { 123 | ERROR("png_create_info_struct"); 124 | } 125 | 126 | if (setjmp(png_jmpbuf(png))) { 127 | ERROR("png_jmpbuf"); 128 | } 129 | 130 | // Output is 8bit depth, RGBA format. 131 | png_set_IHDR( 132 | png, 133 | info, 134 | w, h, 135 | 8, 136 | channels == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGBA, 137 | PNG_INTERLACE_NONE, 138 | PNG_COMPRESSION_TYPE_DEFAULT, 139 | PNG_FILTER_TYPE_DEFAULT 140 | ); 141 | 142 | png_bytep row_pointers[h]; 143 | for(int y = 0; y < h; y++){ 144 | row_pointers[y] = ((unsigned char *)pixels + y * w * channels); 145 | } 146 | 147 | libpng_write_t write_data = { 148 | .size = 0, 149 | .capacity = w * h * channels, 150 | .data = malloc(w * h * channels) 151 | }; 152 | 153 | png_set_rows(png, info, row_pointers); 154 | png_set_write_fn(png, &write_data, libpng_encode_callback, NULL); 155 | png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL); 156 | 157 | png_destroy_write_struct(&png, &info); 158 | 159 | *out_len = write_data.size; 160 | return write_data.data; 161 | } 162 | 163 | 164 | typedef struct { 165 | int pos; 166 | int size; 167 | unsigned char *data; 168 | } libpng_read_t; 169 | 170 | void png_decode_callback(png_structp png, png_bytep data, png_size_t length) { 171 | libpng_read_t *read_data = (libpng_read_t*)png_get_io_ptr(png); 172 | if (read_data->pos + length > read_data->size) { 173 | ERROR("PNG read %ld bytes at pos %d (size: %d)", length, read_data->pos, read_data->size); 174 | } 175 | memcpy(data, read_data->data + read_data->pos, length); 176 | read_data->pos += length; 177 | } 178 | 179 | void png_warning_callback(png_structp png_ptr, png_const_charp warning_msg) { 180 | // Ignore warnings about sRGB profiles and such. 181 | } 182 | 183 | void *libpng_decode(void *data, int size, int *out_w, int *out_h) { 184 | png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, png_warning_callback); 185 | if (!png) { 186 | ERROR("png_create_read_struct"); 187 | } 188 | 189 | png_infop info = png_create_info_struct(png); 190 | if (!info) { 191 | ERROR("png_create_info_struct"); 192 | } 193 | 194 | libpng_read_t read_data = { 195 | .pos = 0, 196 | .size = size, 197 | .data = data 198 | }; 199 | 200 | png_set_read_fn(png, &read_data, png_decode_callback); 201 | png_set_sig_bytes(png, 0); 202 | png_read_info(png, info); 203 | 204 | png_uint_32 w, h; 205 | int bitDepth, colorType, interlaceType; 206 | png_get_IHDR(png, info, &w, &h, &bitDepth, &colorType, &interlaceType, NULL, NULL); 207 | 208 | // 16 bit -> 8 bit 209 | png_set_strip_16(png); 210 | 211 | // 1, 2, 4 bit -> 8 bit 212 | if (bitDepth < 8) { 213 | png_set_packing(png); 214 | } 215 | 216 | if (colorType & PNG_COLOR_MASK_PALETTE) { 217 | png_set_expand(png); 218 | } 219 | 220 | if (!(colorType & PNG_COLOR_MASK_COLOR)) { 221 | png_set_gray_to_rgb(png); 222 | } 223 | 224 | // set paletted or RGB images with transparency to full alpha so we get RGBA 225 | if (png_get_valid(png, info, PNG_INFO_tRNS)) { 226 | png_set_tRNS_to_alpha(png); 227 | } 228 | 229 | // make sure every pixel has an alpha value 230 | if (!(colorType & PNG_COLOR_MASK_ALPHA)) { 231 | png_set_filler(png, 255, PNG_FILLER_AFTER); 232 | } 233 | 234 | png_read_update_info(png, info); 235 | 236 | unsigned char* out = malloc(w * h * 4); 237 | *out_w = w; 238 | *out_h = h; 239 | 240 | // png_uint_32 rowBytes = png_get_rowbytes(png, info); 241 | png_bytep row_pointers[h]; 242 | for (png_uint_32 row = 0; row < h; row++ ) { 243 | row_pointers[row] = (png_bytep)(out + (row * w * 4)); 244 | } 245 | 246 | png_read_image(png, row_pointers); 247 | png_read_end(png, info); 248 | png_destroy_read_struct( &png, &info, NULL); 249 | 250 | return out; 251 | } 252 | 253 | 254 | // ----------------------------------------------------------------------------- 255 | // stb_image encode callback 256 | 257 | void stbi_write_callback(void *context, void *data, int size) { 258 | int *encoded_size = (int *)context; 259 | *encoded_size += size; 260 | // In theory we'd need to do another malloc(), memcpy() and free() here to 261 | // be fair to the other decode functions... 262 | } 263 | 264 | 265 | // ----------------------------------------------------------------------------- 266 | // function to load a whole file into memory 267 | 268 | void *fload(const char *path, int *out_size) { 269 | FILE *fh = fopen(path, "rb"); 270 | if (!fh) { 271 | ERROR("Can't open file"); 272 | } 273 | 274 | fseek(fh, 0, SEEK_END); 275 | int size = ftell(fh); 276 | fseek(fh, 0, SEEK_SET); 277 | 278 | void *buffer = malloc(size); 279 | if (!buffer) { 280 | ERROR("Malloc for %d bytes failed", size); 281 | } 282 | 283 | if (!fread(buffer, size, 1, fh)) { 284 | ERROR("Can't read file %s", path); 285 | } 286 | fclose(fh); 287 | 288 | *out_size = size; 289 | return buffer; 290 | } 291 | 292 | 293 | // ----------------------------------------------------------------------------- 294 | // benchmark runner 295 | 296 | 297 | int opt_runs = 1; 298 | int opt_nopng = 0; 299 | int opt_nowarmup = 0; 300 | int opt_noverify = 0; 301 | int opt_nodecode = 0; 302 | int opt_noencode = 0; 303 | int opt_norecurse = 0; 304 | int opt_onlytotals = 0; 305 | 306 | 307 | typedef struct { 308 | uint64_t size; 309 | uint64_t encode_time; 310 | uint64_t decode_time; 311 | } benchmark_lib_result_t; 312 | 313 | typedef struct { 314 | int count; 315 | uint64_t raw_size; 316 | uint64_t px; 317 | int w; 318 | int h; 319 | benchmark_lib_result_t libpng; 320 | benchmark_lib_result_t stbi; 321 | benchmark_lib_result_t qoi; 322 | } benchmark_result_t; 323 | 324 | 325 | void benchmark_print_result(benchmark_result_t res) { 326 | res.px /= res.count; 327 | res.raw_size /= res.count; 328 | res.libpng.encode_time /= res.count; 329 | res.libpng.decode_time /= res.count; 330 | res.libpng.size /= res.count; 331 | res.stbi.encode_time /= res.count; 332 | res.stbi.decode_time /= res.count; 333 | res.stbi.size /= res.count; 334 | res.qoi.encode_time /= res.count; 335 | res.qoi.decode_time /= res.count; 336 | res.qoi.size /= res.count; 337 | 338 | double px = res.px; 339 | printf(" decode ms encode ms decode mpps encode mpps size kb rate\n"); 340 | if (!opt_nopng) { 341 | printf( 342 | "libpng: %8.1f %8.1f %8.2f %8.2f %8ld %4.1f%%\n", 343 | (double)res.libpng.decode_time/1000000.0, 344 | (double)res.libpng.encode_time/1000000.0, 345 | (res.libpng.decode_time > 0 ? px / ((double)res.libpng.decode_time/1000.0) : 0), 346 | (res.libpng.encode_time > 0 ? px / ((double)res.libpng.encode_time/1000.0) : 0), 347 | res.libpng.size/1024, 348 | ((double)res.libpng.size/(double)res.raw_size) * 100.0 349 | ); 350 | printf( 351 | "stbi: %8.1f %8.1f %8.2f %8.2f %8ld %4.1f%%\n", 352 | (double)res.stbi.decode_time/1000000.0, 353 | (double)res.stbi.encode_time/1000000.0, 354 | (res.stbi.decode_time > 0 ? px / ((double)res.stbi.decode_time/1000.0) : 0), 355 | (res.stbi.encode_time > 0 ? px / ((double)res.stbi.encode_time/1000.0) : 0), 356 | res.stbi.size/1024, 357 | ((double)res.stbi.size/(double)res.raw_size) * 100.0 358 | ); 359 | } 360 | printf( 361 | "qoi: %8.1f %8.1f %8.2f %8.2f %8ld %4.1f%%\n", 362 | (double)res.qoi.decode_time/1000000.0, 363 | (double)res.qoi.encode_time/1000000.0, 364 | (res.qoi.decode_time > 0 ? px / ((double)res.qoi.decode_time/1000.0) : 0), 365 | (res.qoi.encode_time > 0 ? px / ((double)res.qoi.encode_time/1000.0) : 0), 366 | res.qoi.size/1024, 367 | ((double)res.qoi.size/(double)res.raw_size) * 100.0 368 | ); 369 | printf("\n"); 370 | } 371 | 372 | // Run __VA_ARGS__ a number of times and measure the time taken. The first 373 | // run is ignored. 374 | #define BENCHMARK_FN(NOWARMUP, RUNS, AVG_TIME, ...) \ 375 | do { \ 376 | uint64_t time = 0; \ 377 | for (int i = NOWARMUP; i <= RUNS; i++) { \ 378 | uint64_t time_start = ns(); \ 379 | __VA_ARGS__ \ 380 | uint64_t time_end = ns(); \ 381 | if (i > 0) { \ 382 | time += time_end - time_start; \ 383 | } \ 384 | } \ 385 | AVG_TIME = time / RUNS; \ 386 | } while (0) 387 | 388 | 389 | benchmark_result_t benchmark_image(const char *path) { 390 | int encoded_png_size; 391 | int encoded_qoi_size; 392 | int w; 393 | int h; 394 | int channels; 395 | 396 | // Load the encoded PNG, encoded QOI and raw pixels into memory 397 | if(!stbi_info(path, &w, &h, &channels)) { 398 | ERROR("Error decoding header %s", path); 399 | } 400 | 401 | if (channels != 3) { 402 | channels = 4; 403 | } 404 | 405 | void *pixels = (void *)stbi_load(path, &w, &h, NULL, channels); 406 | void *encoded_png = fload(path, &encoded_png_size); 407 | void *encoded_qoi = qoi_encode(pixels, &(qoi_desc){ 408 | .width = w, 409 | .height = h, 410 | .channels = channels, 411 | .colorspace = QOI_SRGB 412 | }, &encoded_qoi_size); 413 | 414 | if (!pixels || !encoded_qoi || !encoded_png) { 415 | ERROR("Error encoding %s", path); 416 | } 417 | 418 | // Verify QOI Output 419 | 420 | if (!opt_noverify) { 421 | qoi_desc dc; 422 | void *pixels_qoi = qoi_decode(encoded_qoi, encoded_qoi_size, &dc, channels); 423 | if (memcmp(pixels, pixels_qoi, w * h * channels) != 0) { 424 | ERROR("QOI roundtrip pixel mismatch for %s", path); 425 | } 426 | free(pixels_qoi); 427 | } 428 | 429 | 430 | 431 | benchmark_result_t res = {0}; 432 | res.count = 1; 433 | res.raw_size = w * h * channels; 434 | res.px = w * h; 435 | res.w = w; 436 | res.h = h; 437 | 438 | 439 | // Decoding 440 | 441 | if (!opt_nodecode) { 442 | if (!opt_nopng) { 443 | BENCHMARK_FN(opt_nowarmup, opt_runs, res.libpng.decode_time, { 444 | int dec_w, dec_h; 445 | void *dec_p = libpng_decode(encoded_png, encoded_png_size, &dec_w, &dec_h); 446 | free(dec_p); 447 | }); 448 | 449 | BENCHMARK_FN(opt_nowarmup, opt_runs, res.stbi.decode_time, { 450 | int dec_w, dec_h, dec_channels; 451 | void *dec_p = stbi_load_from_memory(encoded_png, encoded_png_size, &dec_w, &dec_h, &dec_channels, 4); 452 | free(dec_p); 453 | }); 454 | } 455 | 456 | BENCHMARK_FN(opt_nowarmup, opt_runs, res.qoi.decode_time, { 457 | qoi_desc desc; 458 | void *dec_p = qoi_decode(encoded_qoi, encoded_qoi_size, &desc, 4); 459 | free(dec_p); 460 | }); 461 | } 462 | 463 | 464 | // Encoding 465 | if (!opt_noencode) { 466 | if (!opt_nopng) { 467 | BENCHMARK_FN(opt_nowarmup, opt_runs, res.libpng.encode_time, { 468 | int enc_size; 469 | void *enc_p = libpng_encode(pixels, w, h, channels, &enc_size); 470 | res.libpng.size = enc_size; 471 | free(enc_p); 472 | }); 473 | 474 | BENCHMARK_FN(opt_nowarmup, opt_runs, res.stbi.encode_time, { 475 | int enc_size = 0; 476 | stbi_write_png_to_func(stbi_write_callback, &enc_size, w, h, channels, pixels, 0); 477 | res.stbi.size = enc_size; 478 | }); 479 | } 480 | 481 | BENCHMARK_FN(opt_nowarmup, opt_runs, res.qoi.encode_time, { 482 | int enc_size; 483 | void *enc_p = qoi_encode(pixels, &(qoi_desc){ 484 | .width = w, 485 | .height = h, 486 | .channels = channels, 487 | .colorspace = QOI_SRGB 488 | }, &enc_size); 489 | res.qoi.size = enc_size; 490 | free(enc_p); 491 | }); 492 | } 493 | 494 | free(pixels); 495 | free(encoded_png); 496 | free(encoded_qoi); 497 | 498 | return res; 499 | } 500 | 501 | void benchmark_directory(const char *path, benchmark_result_t *grand_total) { 502 | DIR *dir = opendir(path); 503 | if (!dir) { 504 | ERROR("Couldn't open directory %s", path); 505 | } 506 | 507 | struct dirent *file; 508 | 509 | if (!opt_norecurse) { 510 | for (int i = 0; (file = readdir(dir)) != NULL; i++) { 511 | if ( 512 | file->d_type & DT_DIR && 513 | strcmp(file->d_name, ".") != 0 && 514 | strcmp(file->d_name, "..") != 0 515 | ) { 516 | char subpath[1024]; 517 | snprintf(subpath, 1024, "%s/%s", path, file->d_name); 518 | benchmark_directory(subpath, grand_total); 519 | } 520 | } 521 | rewinddir(dir); 522 | } 523 | 524 | benchmark_result_t dir_total = {0}; 525 | 526 | int has_shown_head = 0; 527 | for (int i = 0; (file = readdir(dir)) != NULL; i++) { 528 | if (strcmp(file->d_name + strlen(file->d_name) - 4, ".png") != 0) { 529 | continue; 530 | } 531 | 532 | if (!has_shown_head) { 533 | has_shown_head = 1; 534 | printf("## Benchmarking %s/*.png -- %d runs\n\n", path, opt_runs); 535 | } 536 | 537 | char *file_path = malloc(strlen(file->d_name) + strlen(path)+8); 538 | sprintf(file_path, "%s/%s", path, file->d_name); 539 | 540 | benchmark_result_t res = benchmark_image(file_path); 541 | 542 | if (!opt_onlytotals) { 543 | printf("## %s size: %dx%d\n", file_path, res.w, res.h); 544 | benchmark_print_result(res); 545 | } 546 | 547 | free(file_path); 548 | 549 | dir_total.count++; 550 | dir_total.raw_size += res.raw_size; 551 | dir_total.px += res.px; 552 | dir_total.libpng.encode_time += res.libpng.encode_time; 553 | dir_total.libpng.decode_time += res.libpng.decode_time; 554 | dir_total.libpng.size += res.libpng.size; 555 | dir_total.stbi.encode_time += res.stbi.encode_time; 556 | dir_total.stbi.decode_time += res.stbi.decode_time; 557 | dir_total.stbi.size += res.stbi.size; 558 | dir_total.qoi.encode_time += res.qoi.encode_time; 559 | dir_total.qoi.decode_time += res.qoi.decode_time; 560 | dir_total.qoi.size += res.qoi.size; 561 | 562 | grand_total->count++; 563 | grand_total->raw_size += res.raw_size; 564 | grand_total->px += res.px; 565 | grand_total->libpng.encode_time += res.libpng.encode_time; 566 | grand_total->libpng.decode_time += res.libpng.decode_time; 567 | grand_total->libpng.size += res.libpng.size; 568 | grand_total->stbi.encode_time += res.stbi.encode_time; 569 | grand_total->stbi.decode_time += res.stbi.decode_time; 570 | grand_total->stbi.size += res.stbi.size; 571 | grand_total->qoi.encode_time += res.qoi.encode_time; 572 | grand_total->qoi.decode_time += res.qoi.decode_time; 573 | grand_total->qoi.size += res.qoi.size; 574 | } 575 | closedir(dir); 576 | 577 | if (dir_total.count > 0) { 578 | printf("## Total for %s\n", path); 579 | benchmark_print_result(dir_total); 580 | } 581 | } 582 | 583 | int main(int argc, char **argv) { 584 | if (argc < 3) { 585 | printf("Usage: qoibench [options]\n"); 586 | printf("Options:\n"); 587 | printf(" --nowarmup ... don't perform a warmup run\n"); 588 | printf(" --nopng ...... don't run png encode/decode\n"); 589 | printf(" --noverify ... don't verify qoi roundtrip\n"); 590 | printf(" --noencode ... don't run encoders\n"); 591 | printf(" --nodecode ... don't run decoders\n"); 592 | printf(" --norecurse .. don't descend into directories\n"); 593 | printf(" --onlytotals . don't print individual image results\n"); 594 | printf("Examples\n"); 595 | printf(" qoibench 10 images/textures/\n"); 596 | printf(" qoibench 1 images/textures/ --nopng --nowarmup\n"); 597 | exit(1); 598 | } 599 | 600 | for (int i = 3; i < argc; i++) { 601 | if (strcmp(argv[i], "--nowarmup") == 0) { opt_nowarmup = 1; } 602 | else if (strcmp(argv[i], "--nopng") == 0) { opt_nopng = 1; } 603 | else if (strcmp(argv[i], "--noverify") == 0) { opt_noverify = 1; } 604 | else if (strcmp(argv[i], "--noencode") == 0) { opt_noencode = 1; } 605 | else if (strcmp(argv[i], "--nodecode") == 0) { opt_nodecode = 1; } 606 | else if (strcmp(argv[i], "--norecurse") == 0) { opt_norecurse = 1; } 607 | else if (strcmp(argv[i], "--onlytotals") == 0) { opt_onlytotals = 1; } 608 | else { ERROR("Unknown option %s", argv[i]); } 609 | } 610 | 611 | opt_runs = atoi(argv[1]); 612 | if (opt_runs <=0) { 613 | ERROR("Invalid number of runs %d", opt_runs); 614 | } 615 | 616 | benchmark_result_t grand_total = {0}; 617 | benchmark_directory(argv[2], &grand_total); 618 | 619 | if (grand_total.count > 0) { 620 | printf("# Grand total for %s\n", argv[2]); 621 | benchmark_print_result(grand_total); 622 | } 623 | else { 624 | printf("No images found in %s\n", argv[2]); 625 | } 626 | 627 | return 0; 628 | } 629 | -------------------------------------------------------------------------------- /qoiconv.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | 7 | Command line tool to convert between png <> qoi format 8 | 9 | Requires: 10 | -"stb_image.h" (https://github.com/nothings/stb/blob/master/stb_image.h) 11 | -"stb_image_write.h" (https://github.com/nothings/stb/blob/master/stb_image_write.h) 12 | -"qoi.h" (https://github.com/phoboslab/qoi/blob/master/qoi.h) 13 | 14 | Compile with: 15 | gcc qoiconv.c -std=c99 -O3 -o qoiconv 16 | 17 | */ 18 | 19 | 20 | #define STB_IMAGE_IMPLEMENTATION 21 | #define STBI_ONLY_PNG 22 | #define STBI_NO_LINEAR 23 | #include "stb_image.h" 24 | 25 | #define STB_IMAGE_WRITE_IMPLEMENTATION 26 | #include "stb_image_write.h" 27 | 28 | #define QOI_IMPLEMENTATION 29 | #include "qoi.h" 30 | 31 | 32 | #define STR_ENDS_WITH(S, E) (strcmp(S + strlen(S) - (sizeof(E)-1), E) == 0) 33 | 34 | int main(int argc, char **argv) { 35 | if (argc < 3) { 36 | puts("Usage: qoiconv "); 37 | puts("Examples:"); 38 | puts(" qoiconv input.png output.qoi"); 39 | puts(" qoiconv input.qoi output.png"); 40 | exit(1); 41 | } 42 | 43 | void *pixels = NULL; 44 | int w, h, channels; 45 | if (STR_ENDS_WITH(argv[1], ".png")) { 46 | if(!stbi_info(argv[1], &w, &h, &channels)) { 47 | printf("Couldn't read header %s\n", argv[1]); 48 | exit(1); 49 | } 50 | 51 | // Force all odd encodings to be RGBA 52 | if(channels != 3) { 53 | channels = 4; 54 | } 55 | 56 | pixels = (void *)stbi_load(argv[1], &w, &h, NULL, channels); 57 | } 58 | else if (STR_ENDS_WITH(argv[1], ".qoi")) { 59 | qoi_desc desc; 60 | pixels = qoi_read(argv[1], &desc, 0); 61 | channels = desc.channels; 62 | w = desc.width; 63 | h = desc.height; 64 | } 65 | 66 | if (pixels == NULL) { 67 | printf("Couldn't load/decode %s\n", argv[1]); 68 | exit(1); 69 | } 70 | 71 | int encoded = 0; 72 | if (STR_ENDS_WITH(argv[2], ".png")) { 73 | encoded = stbi_write_png(argv[2], w, h, channels, pixels, 0); 74 | } 75 | else if (STR_ENDS_WITH(argv[2], ".qoi")) { 76 | encoded = qoi_write(argv[2], pixels, &(qoi_desc){ 77 | .width = w, 78 | .height = h, 79 | .channels = channels, 80 | .colorspace = QOI_SRGB 81 | }); 82 | } 83 | 84 | if (!encoded) { 85 | printf("Couldn't write/encode %s\n", argv[2]); 86 | exit(1); 87 | } 88 | 89 | free(pixels); 90 | return 0; 91 | } 92 | -------------------------------------------------------------------------------- /qoifuzz.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | 7 | clang fuzzing harness for qoi_decode 8 | 9 | Compile and run with: 10 | clang -fsanitize=address,fuzzer -g -O0 qoifuzz.c && ./a.out 11 | 12 | */ 13 | 14 | 15 | #define QOI_IMPLEMENTATION 16 | #include "qoi.h" 17 | #include 18 | #include 19 | 20 | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { 21 | int w, h; 22 | if (size < 4) { 23 | return 0; 24 | } 25 | 26 | qoi_desc desc; 27 | void* decoded = qoi_decode((void*)(data + 4), (int)(size - 4), &desc, *((int *)data)); 28 | if (decoded != NULL) { 29 | free(decoded); 30 | } 31 | return 0; 32 | } 33 | -------------------------------------------------------------------------------- /verilog/qoi_decoder.v: -------------------------------------------------------------------------------- 1 | `define QOI_OP_INDEX 2'b00 /* 0x00 */ 2 | `define QOI_OP_DIFF 2'b01 /* 0x40 */ 3 | `define QOI_OP_LUMA 2'b10 /* 0x80 */ 4 | `define QOI_OP_RUN 2'b11 /* 0xc0 */ 5 | `define QOI_OP_RGB 8'b11111110 /* 0xfe */ 6 | `define QOI_OP_RGBA 8'b11111111 /* 0xff */ 7 | 8 | module qoi_decoder( 9 | input reg[7:0] chunk[4:0], 10 | output reg[2:0] chunk_len_consumed, 11 | 12 | input wire clk, 13 | input wire rst, 14 | 15 | output wire[7:0] r, 16 | output wire[7:0] g, 17 | output wire[7:0] b, 18 | output wire[7:0] a 19 | ); 20 | 21 | wire[2:0] shortop = chunk[0][7:6]; 22 | 23 | reg[7:0] next_r; 24 | reg[7:0] next_g; 25 | reg[7:0] next_b; 26 | reg[7:0] next_a; 27 | reg[2:0] next_chunk_len_consumed; 28 | 29 | // QOI_OP_INDEX 30 | // previously seen pixel array, indexed by hash of the color 31 | reg[31:0] index[63:0]; 32 | wire[5:0] index_pos = next_r * 3 + next_g * 5 + next_b * 7 + next_a * 11; 33 | 34 | // QOI_OP_DIFF 35 | wire signed[1:0] vr = chunk[0][5:4] - 2; 36 | wire signed[1:0] vg = chunk[0][3:2] - 2; 37 | wire signed[1:0] vb = chunk[0][1:0] - 2; 38 | 39 | // QOI_OP_LUMA 40 | // Reference implementation software calls "luma" variables "vg" too, but we 41 | // can't since we want different sized wires between the OPs. 42 | wire signed[5:0] luma = chunk[0][5:0] - 32; 43 | wire signed[3:0] luma_r = chunk[1][7:4] - 8; 44 | wire signed[3:0] luma_b = chunk[1][3:0] - 8; 45 | 46 | // QOI_OP_RUN counters 47 | reg[5:0] run; 48 | reg[5:0] next_run; 49 | // The spec has a few illegal run lengths, so let's re-purpose one of them 50 | `define NOT_IN_A_RUN 'b111111 51 | 52 | always @ * begin 53 | next_r = r; 54 | next_g = g; 55 | next_b = b; 56 | next_a = a; 57 | 58 | next_run = run; 59 | 60 | if (chunk[0] == `QOI_OP_RGB) begin 61 | {next_r, next_g, next_b} = {chunk[1], chunk[2], chunk[3]}; 62 | next_chunk_len_consumed = 4; 63 | 64 | end else if (chunk[0] == `QOI_OP_RGBA) begin 65 | {next_r, next_g, next_b, next_a} = {chunk[1], chunk[2], chunk[3], chunk[4]}; 66 | next_chunk_len_consumed = 5; 67 | 68 | end else if (shortop == `QOI_OP_INDEX) begin 69 | {next_r, next_g, next_b, next_a} = index[chunk[0][5:0]]; 70 | next_chunk_len_consumed = 1; 71 | 72 | end else if (shortop == `QOI_OP_DIFF) begin 73 | next_r = signed'(r) + vr; 74 | next_g = signed'(g) + vg; 75 | next_b = signed'(b) + vb; 76 | next_chunk_len_consumed = 1; 77 | 78 | end else if (shortop == `QOI_OP_LUMA) begin 79 | next_r = signed'(r) + luma + luma_r; 80 | next_g = signed'(g) + luma; 81 | next_b = signed'(b) + luma + luma_b; 82 | next_chunk_len_consumed = 2; 83 | 84 | end else if (shortop == `QOI_OP_RUN) begin 85 | if (run == `NOT_IN_A_RUN) begin // Is this run new? 86 | next_run = chunk[0][5:0]; 87 | end else begin 88 | next_run--; 89 | end 90 | 91 | if (next_run > 0) begin 92 | // We still have pixels to go, don't consume chunk yet 93 | next_chunk_len_consumed = 0; 94 | end else begin 95 | // Finish run 96 | next_run = `NOT_IN_A_RUN; 97 | next_chunk_len_consumed = 1; 98 | end 99 | 100 | end else begin 101 | $error("Uncaught QOI_OP decoding. chunk[0]=%h", chunk[0]); 102 | {next_r, next_g, next_b, next_a} = 'h_deadbeef; 103 | next_chunk_len_consumed = 0; 104 | end 105 | end 106 | 107 | always @ (posedge clk) begin 108 | chunk_len_consumed <= next_chunk_len_consumed; 109 | {r, g, b, a} <= {next_r, next_g, next_b, next_a}; 110 | 111 | index[index_pos] <= {next_r, next_g, next_b, next_a}; 112 | run <= next_run; 113 | 114 | if (rst) begin 115 | chunk_len_consumed <= 0; 116 | 117 | r <= 0; 118 | g <= 0; 119 | b <= 0; 120 | a <= 255; 121 | 122 | index <= '{default: 0}; 123 | run <= `NOT_IN_A_RUN; 124 | end 125 | end 126 | 127 | endmodule 128 | -------------------------------------------------------------------------------- /verilog/qoi_encoder.v: -------------------------------------------------------------------------------- 1 | `define QOI_OP_INDEX 2'b00 /* 0x00 */ 2 | `define QOI_OP_DIFF 2'b01 /* 0x40 */ 3 | `define QOI_OP_LUMA 2'b10 /* 0x80 */ 4 | `define QOI_OP_RUN 2'b11 /* 0xc0 */ 5 | `define QOI_OP_RGB 8'b11111110 /* 0xfe */ 6 | `define QOI_OP_RGBA 8'b11111111 /* 0xff */ 7 | 8 | module qoi_encoder( 9 | input wire[7:0] r, 10 | input wire[7:0] g, 11 | input wire[7:0] b, 12 | input wire[7:0] a, 13 | 14 | input wire finish, 15 | 16 | input wire clk, 17 | input wire rst, 18 | 19 | output reg[7:0] chunk[4:0], 20 | output reg[2:0] chunk_len 21 | ); 22 | 23 | wire[31:0] px = {r, g, b, a}; 24 | 25 | // QOI_OP_DIFF + QOI_OP_LUMA 26 | reg[7:0] prev_r; 27 | reg[7:0] prev_g; 28 | reg[7:0] prev_b; 29 | 30 | wire signed[7:0] vr = r - prev_r; 31 | wire signed[7:0] vg = g - prev_g; 32 | wire signed[7:0] vb = b - prev_b; 33 | 34 | // QOI_OP_LUMA 35 | wire signed[7:0] vg_r = vr - vg; 36 | wire signed[7:0] vg_b = vb - vg; 37 | 38 | // QOI_OP_RGBA 39 | reg[7:0] prev_a; 40 | 41 | // QOI_OP_RUN 42 | // We need to delay chunk output by one pixel because we cannot emit 2 chunks 43 | // per pixel like software does when the run ends. 44 | reg prev_finish; // aka past-end 45 | wire is_repeating = ({prev_r, prev_g, prev_b, prev_a} == px) && !finish; 46 | reg[7:0] next_chunk[4:0]; 47 | reg[2:0] next_chunk_len; 48 | reg[5:0] run; 49 | 50 | // QOI_OP_INDEX 51 | // previously seen pixel array, indexed by hash of the color 52 | reg[31:0] index[63:0]; 53 | wire[5:0] index_pos = r * 3 + g * 5 + b * 7 + a * 11; 54 | 55 | always @ (posedge clk) begin 56 | if (is_repeating) begin : start_and_midde_OP_RUN 57 | // For debugging: uncomment. For power-saving: comment 58 | next_chunk[0] <= {`QOI_OP_RUN, run}; // Dummy 59 | // This chunk is not over, let output know not to expect anything yet 60 | next_chunk_len <= 0; 61 | run <= run + 1; 62 | 63 | end else if (index[index_pos] == px) begin 64 | next_chunk[0] <= {`QOI_OP_INDEX, index_pos}; 65 | next_chunk_len <= 1; 66 | 67 | end else if (prev_a != a) begin 68 | next_chunk[0] <= `QOI_OP_RGBA; 69 | next_chunk[1] <= r; 70 | next_chunk[2] <= g; 71 | next_chunk[3] <= b; 72 | next_chunk[4] <= a; 73 | next_chunk_len <= 5; 74 | 75 | end else if ( 76 | vr > -3 && vr < 2 && 77 | vg > -3 && vg < 2 && 78 | vb > -3 && vb < 2 79 | ) begin 80 | next_chunk[0] <= {`QOI_OP_DIFF, 2'(vr + 2), 2'(vg + 2), 2'(vb + 2)}; 81 | next_chunk_len <= 1; 82 | 83 | end else if ( 84 | vg_r > -9 && vg_r < 8 && 85 | vg > -33 && vg < 32 && 86 | vg_b > -9 && vg_b < 8 87 | ) begin 88 | next_chunk[0] <= {`QOI_OP_LUMA, 6'(vg + 32)}; 89 | next_chunk[1] <= {4'(vg_r + 8), 4'(vg_b + 8)}; 90 | next_chunk_len <= 2; 91 | 92 | end else begin 93 | next_chunk[0] <= `QOI_OP_RGB; 94 | next_chunk[1] <= r; 95 | next_chunk[2] <= g; 96 | next_chunk[3] <= b; 97 | next_chunk_len <= 4; 98 | end 99 | 100 | prev_r <= r; 101 | prev_g <= g; 102 | prev_b <= b; 103 | prev_a <= a; 104 | prev_finish <= finish; 105 | 106 | index[index_pos] <= px; 107 | 108 | chunk <= next_chunk; 109 | chunk_len <= next_chunk_len; 110 | if (((run > 0) && !is_repeating) || (run == 62)) begin : commit_OP_RUN 111 | run <= is_repeating; // count the current repeat, otherwise start from 0 112 | chunk[0] <= {`QOI_OP_RUN, 6'(run - 1)}; 113 | chunk_len <= 1; 114 | end : commit_OP_RUN 115 | 116 | if (prev_finish) begin 117 | // to be nice, make sure we don't output extra bogus chunks after we 118 | // emitted the last real chunk shortly after the last pixel in the stream 119 | chunk_len <= 0; 120 | end 121 | 122 | if (rst) begin 123 | prev_r <= 0; 124 | prev_g <= 0; 125 | prev_b <= 0; 126 | prev_a <= 255; 127 | 128 | chunk <= '{default:0}; 129 | chunk_len <= 0; 130 | 131 | next_chunk <= '{default:0}; 132 | next_chunk_len <= 0; 133 | 134 | run <= 0; 135 | 136 | index <= '{default: 0}; 137 | end 138 | end 139 | 140 | endmodule 141 | --------------------------------------------------------------------------------