From 66eed8b7548c3e772d3bcb0d351608f0df81ecaf Mon Sep 17 00:00:00 2001 From: Frederico Linhares Date: Thu, 22 Jun 2023 19:13:07 -0300 Subject: feat Implement an algorithm to decode QOI * src/vk/qoi.cpp: Implement an algorithm to decode QOI. This eliminates an unnecessary dependency as the engine does not need to encode QOI files, only decode them. --- src/vk/qoi.cpp | 246 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/vk/qoi.hpp | 41 +++++++++ src/vk/texture.cpp | 23 ++--- 3 files changed, 294 insertions(+), 16 deletions(-) create mode 100644 src/vk/qoi.cpp create mode 100644 src/vk/qoi.hpp (limited to 'src/vk') diff --git a/src/vk/qoi.cpp b/src/vk/qoi.cpp new file mode 100644 index 0000000..8b7ef9e --- /dev/null +++ b/src/vk/qoi.cpp @@ -0,0 +1,246 @@ +/* + * Copyright 2022-2023 Frederico de Oliveira Linhares + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "qoi.hpp" + +#include +#include + +namespace +{ + +const char MAGIC[]{"qoif"}; +const int HEADER_SIZE{14}; +const uint8_t PADDING[8]{0,0,0,0,0,0,0,1}; +const unsigned int PIXELS_MAX{400000000}; + +const int OP_INDEX{0b00000000}; +const int OP_DIFF {0b01000000}; +const int OP_LUMA {0b10000000}; +const int OP_RUN {0b11000000}; +const int OP_RGB {0b11111110}; +const int OP_RGBA {0b11111111}; + +const int MASK_2_BITS{0b11000000}; + +struct RGBA +{ + uint8_t red, green, blue, alpha; + + RGBA(); + RGBA(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha); +}; + +RGBA::RGBA(): + red{0}, + green{0}, + blue{0}, + alpha{255} +{ +} + +RGBA::RGBA(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha): + red{red}, + green{green}, + blue{blue}, + alpha{alpha} +{ +} + +struct Pixel +{ + union + { + RGBA colors; + uint32_t value; + }; + + Pixel(); + Pixel(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha); +}; + +Pixel::Pixel(): + colors{} +{ +} + +Pixel::Pixel(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha): + colors(red, green, blue, alpha) +{ +} + +inline int +color_hash(const RGBA &colors) +{ + return colors.red*3 + colors.green*5 + colors.blue*7 + colors.alpha*11; +} + +void +read_chars(uint8_t *data, int &p, char *str, int size) +{ + for(int i{0}; i < size; i++) str[i] = (char)data[p++]; +} + +void +read_ui8(uint8_t *data, int &p, uint8_t &num) +{ + num = data[p++]; +} + +void +read_ui32(uint8_t *data, int &p, uint32_t &num) +{ + uint8_t b1{data[p++]}, b2{data[p++]}, b3{data[p++]}, b4{data[p++]}; + + num = b1 << 24 | b2 << 16 | b3 << 8 | b4; +} + +} + +namespace VK::QOI +{ + +Image::Image(const char *file_path, uint8_t channels): + channels{channels} +{ + int data_p{0}; + int data_size; + uint8_t *data; + + if(this->channels != 0 && this->channels != 3 && this->channels != 4) + { + throw std::invalid_argument{"invalid number of channels"}; + } + + { // Read data from file. + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if(!file) + { + throw std::runtime_error{"failed to open QOI file"}; + } + + data_size = file.tellg(); + if(data_size < HEADER_SIZE + (int)sizeof(PADDING)) + { + throw std::runtime_error{"invalid QOI file"}; + } + + file.seekg(0); + data = new uint8_t[data_size]; + file.read((char*)data, data_size); + } + + read_chars(data, data_p, this->header.magic, 4); + read_ui32(data, data_p, this->header.width); + read_ui32(data, data_p, this->header.height); + read_ui8(data, data_p, this->header.channels); + read_ui8(data, data_p, this->header.colorspace); + + if(this->header.width == 0 || this->header.height == 0 || + this->header.channels < 3 || this->header.channels > 4 || + this->header.colorspace > 1 || + this->header.height >= PIXELS_MAX / this->header.width) + { + throw std::runtime_error{"QOI file have an invalid header"}; + } + + if(this->channels == 0) this->channels = this->header.channels; + + uint32_t num_pixels{this->header.width * this->header.height}; + this->pixels_len = num_pixels * this->channels; + this->pixels = new uint8_t[this->pixels_len]; + + std::array index; + Pixel pixel{}; + int chunks_len = data_size - (int)sizeof(PADDING); + + /* + This algorithm is based on the original implementation that is in GitHub: + https://github.com/phoboslab/qoi + + For information about the QOI image format, see: https://qoiformat.org/ + */ + int pixel_p{0}; + int run{0}; + for(uint32_t decoded_pixel{0}; decoded_pixel < num_pixels; decoded_pixel++) + { + if(run > 0) + { + run--; + } + else if(data_p < chunks_len) + { + uint8_t output; + int b1; + read_ui8(data, data_p, output); + b1 = output; + + if (b1 == OP_RGB) + { + read_ui8(data, data_p, pixel.colors.red); + read_ui8(data, data_p, pixel.colors.green); + read_ui8(data, data_p, pixel.colors.blue); + } + else if (b1 == OP_RGBA) + { + read_ui8(data, data_p, pixel.colors.red); + read_ui8(data, data_p, pixel.colors.green); + read_ui8(data, data_p, pixel.colors.blue); + read_ui8(data, data_p, pixel.colors.alpha); + } + else if ((b1 & MASK_2_BITS) == OP_INDEX) + { + pixel = index[b1]; + } + else if ((b1 & MASK_2_BITS) == OP_DIFF) + { + pixel.colors.red += ((b1 >> 4) & 0x03) - 2; + pixel.colors.green += ((b1 >> 2) & 0x03) - 2; + pixel.colors.blue += (b1 & 0x03) - 2; + } + else if ((b1 & MASK_2_BITS) == OP_LUMA) + { + int b2; + read_ui8(data, data_p, output); + b2 = output; + int vg = (b1 & 0x3f) - 32; + pixel.colors.red += vg - 8 + ((b2 >> 4) & 0x0f); + pixel.colors.green += vg; + pixel.colors.blue += vg - 8 + (b2 & 0x0f); + } + else if ((b1 & MASK_2_BITS) == OP_RUN) + { + run = (b1 & 0x3f); + } + + index[color_hash(pixel.colors) % 64] = pixel; + } + + this->pixels[pixel_p++] = pixel.colors.red; + this->pixels[pixel_p++] = pixel.colors.green; + this->pixels[pixel_p++] = pixel.colors.blue; + if(this->channels == 4) this->pixels[pixel_p++] = pixel.colors.alpha; + } + + delete[] data; +} + +Image::~Image() +{ + delete[] this->pixels; +} + +} diff --git a/src/vk/qoi.hpp b/src/vk/qoi.hpp new file mode 100644 index 0000000..8a8688f --- /dev/null +++ b/src/vk/qoi.hpp @@ -0,0 +1,41 @@ +/* + * Copyright 2022-2023 Frederico de Oliveira Linhares + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace VK::QOI +{ + struct Header + { + char magic[4]; + uint32_t width; + uint32_t height; + uint8_t channels; + uint8_t colorspace; + }; + + struct Image + { + Header header; + uint32_t pixels_len; + uint8_t channels; + uint8_t *pixels; + + Image(const char *file_path, uint8_t channels); + ~Image(); + }; + +} diff --git a/src/vk/texture.cpp b/src/vk/texture.cpp index c699ecc..92ce5b5 100644 --- a/src/vk/texture.cpp +++ b/src/vk/texture.cpp @@ -16,12 +16,10 @@ #include "texture.hpp" -#define QOI_IMPLEMENTATION 0 -#include - #include "../command.hpp" #include "../core.hpp" #include "image.hpp" +#include "qoi.hpp" #include "source_buffer.hpp" namespace @@ -77,25 +75,21 @@ load_image(void *obj) { auto self = static_cast(obj); - qoi_desc img_desc; const int num_channels = 4; // all images are converted to RGBA - unsigned char *pixels; + VK::QOI::Image qoi_image(self->texture_path.c_str(), num_channels); + uint8_t *pixels; { // Load file image from file. - void *pixels_ptr = qoi_read(self->texture_path.c_str(), &img_desc, 4); - if(pixels_ptr == NULL) throw CommandError{"Failed to open image."}; - - pixels = static_cast(pixels_ptr); - self->texture->width = static_cast(img_desc.width); - self->texture->height = static_cast(img_desc.height); + self->texture->width = qoi_image.header.width; + self->texture->height = qoi_image.header.height; self->texture->mip_levels = 1; - pixels = static_cast(pixels_ptr); + pixels = qoi_image.pixels; } // Load file image into a vulkan buffer. size_t image_size{static_cast( - img_desc.width * img_desc.height * num_channels)}; + qoi_image.header.width * qoi_image.header.height * num_channels)}; VK::SourceBuffer source_image_buffer{ cg_core.vk_device_with_swapchain, pixels, image_size}; @@ -155,9 +149,6 @@ load_image(void *obj) VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); }); } - - // Free resources. - QOI_FREE(pixels); } void -- cgit v1.2.3