repoverse / unify /engine /render /image.cpp
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
3ce1a40 verified
Raw History Blame Contribute Delete
10.4 kB
// QOI ("Quite OK Image") codec for texture assets, and an uncompressed PNG writer for screenshots.
#include "render.h"
#include <cstdio>
#include <cstring>
namespace unify {
static uint32_t qoi_hash(const uint8_t* p) { return (p[0] * 3u + p[1] * 5u + p[2] * 7u + p[3] * 11u) % 64u; }
bool decode_qoi(const std::vector<uint8_t>& d, Image& out) {
if (d.size() < 22 || std::memcmp(d.data(), "qoif", 4)) return false;
auto be32 = [&](size_t o) { return uint32_t(d[o]) << 24 | uint32_t(d[o + 1]) << 16 | uint32_t(d[o + 2]) << 8 | d[o + 3]; };
uint32_t w = be32(4), h = be32(8);
if (w == 0 || h == 0 || w > 16384 || h > 16384) return false;
out = Image(int(w), int(h));
uint8_t index[64 * 4] = {};
uint8_t px[4] = {0, 0, 0, 255};
size_t p = 14, end = d.size() - 8;
int run = 0;
for (size_t i = 0; i < size_t(w) * h; i++) {
if (run > 0) { run--; }
else if (p < end) {
uint8_t b = d[p++];
if (b == 0xFE) { px[0] = d[p]; px[1] = d[p + 1]; px[2] = d[p + 2]; p += 3; }
else if (b == 0xFF) { std::memcpy(px, &d[p], 4); p += 4; }
else if ((b & 0xC0) == 0x00) { std::memcpy(px, &index[(b & 0x3F) * 4], 4); }
else if ((b & 0xC0) == 0x40) { px[0] += ((b >> 4) & 3) - 2; px[1] += ((b >> 2) & 3) - 2; px[2] += (b & 3) - 2; }
else if ((b & 0xC0) == 0x80) {
uint8_t b2 = d[p++];
int vg = (b & 0x3F) - 32;
px[0] += uint8_t(vg - 8 + ((b2 >> 4) & 0xF));
px[1] += uint8_t(vg);
px[2] += uint8_t(vg - 8 + (b2 & 0xF));
} else { run = b & 0x3F; }
std::memcpy(&index[qoi_hash(px) * 4], px, 4);
}
std::memcpy(&out.pixels[i * 4], px, 4);
}
return true;
}
std::vector<uint8_t> encode_qoi(const Image& img) {
std::vector<uint8_t> o;
auto be32 = [&](uint32_t v) { for (int i = 3; i >= 0; i--) o.push_back(uint8_t(v >> (8 * i))); };
o.insert(o.end(), {'q', 'o', 'i', 'f'});
be32(uint32_t(img.width)); be32(uint32_t(img.height));
o.push_back(4); o.push_back(0);
uint8_t index[64 * 4] = {};
uint8_t prev[4] = {0, 0, 0, 255};
int run = 0;
size_t n = size_t(img.width) * img.height;
for (size_t i = 0; i < n; i++) {
const uint8_t* px = &img.pixels[i * 4];
if (!std::memcmp(px, prev, 4)) {
if (++run == 62 || i == n - 1) { o.push_back(uint8_t(0xC0 | (run - 1))); run = 0; }
continue;
}
if (run) { o.push_back(uint8_t(0xC0 | (run - 1))); run = 0; }
uint32_t h = qoi_hash(px);
if (!std::memcmp(&index[h * 4], px, 4)) { o.push_back(uint8_t(h)); }
else {
std::memcpy(&index[h * 4], px, 4);
if (px[3] == prev[3]) {
int8_t vr = int8_t(px[0] - prev[0]), vg = int8_t(px[1] - prev[1]), vb = int8_t(px[2] - prev[2]);
int8_t vgr = int8_t(vr - vg), vgb = int8_t(vb - vg);
if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) o.push_back(uint8_t(0x40 | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2)));
else if (vgr > -9 && vgr < 8 && vg > -33 && vg < 32 && vgb > -9 && vgb < 8) { o.push_back(uint8_t(0x80 | (vg + 32))); o.push_back(uint8_t((vgr + 8) << 4 | (vgb + 8))); }
else { o.push_back(0xFE); o.push_back(px[0]); o.push_back(px[1]); o.push_back(px[2]); }
} else { o.push_back(0xFF); o.insert(o.end(), px, px + 4); }
}
std::memcpy(prev, px, 4);
}
o.insert(o.end(), {0, 0, 0, 0, 0, 0, 0, 1});
return o;
}
bool load_image(const std::string& path, Image& out) {
FILE* f = std::fopen(path.c_str(), "rb");
if (!f) return false;
std::vector<uint8_t> data;
uint8_t buf[65536];
size_t n;
while ((n = std::fread(buf, 1, sizeof buf, f)) > 0) data.insert(data.end(), buf, buf + n);
std::fclose(f);
return decode_qoi(data, out);
}
static uint32_t crc_table[256];
static void init_crc() {
for (uint32_t n = 0; n < 256; n++) { uint32_t c = n; for (int k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320u ^ (c >> 1) : c >> 1; crc_table[n] = c; }
}
static uint32_t crc(const uint8_t* p, size_t n, uint32_t c = 0xFFFFFFFFu) { for (size_t i = 0; i < n; i++) c = crc_table[(c ^ p[i]) & 0xFF] ^ (c >> 8); return c; }
bool write_png(const std::string& path, const Image& img) {
if (!crc_table[1]) init_crc();
// Raw scanlines (filter 0) in stored (uncompressed) deflate blocks inside a zlib stream.
std::vector<uint8_t> raw;
for (int y = 0; y < img.height; y++) { raw.push_back(0); raw.insert(raw.end(), img.at(0, y), img.at(0, y) + img.width * 4); }
std::vector<uint8_t> z = {0x78, 0x01};
uint32_t a = 1, b = 0;
for (uint8_t v : raw) { a = (a + v) % 65521; b = (b + a) % 65521; }
for (size_t off = 0; off < raw.size() || off == 0; off += 65535) {
size_t len = std::min<size_t>(65535, raw.size() - off);
z.push_back(off + len >= raw.size() ? 1 : 0);
z.push_back(uint8_t(len)); z.push_back(uint8_t(len >> 8));
z.push_back(uint8_t(~len)); z.push_back(uint8_t(~len >> 8));
z.insert(z.end(), raw.begin() + long(off), raw.begin() + long(off + len));
if (raw.empty()) break;
}
uint32_t adler = b << 16 | a;
for (int i = 3; i >= 0; i--) z.push_back(uint8_t(adler >> (8 * i)));
FILE* f = std::fopen(path.c_str(), "wb");
if (!f) return false;
auto chunk = [&](const char* type, const std::vector<uint8_t>& data) {
uint8_t len[4] = {uint8_t(data.size() >> 24), uint8_t(data.size() >> 16), uint8_t(data.size() >> 8), uint8_t(data.size())};
std::fwrite(len, 1, 4, f);
std::fwrite(type, 1, 4, f);
if (!data.empty()) std::fwrite(data.data(), 1, data.size(), f);
uint32_t c = crc(reinterpret_cast<const uint8_t*>(type), 4);
c = crc(data.data(), data.size(), c) ^ 0xFFFFFFFFu;
uint8_t cb[4] = {uint8_t(c >> 24), uint8_t(c >> 16), uint8_t(c >> 8), uint8_t(c)};
std::fwrite(cb, 1, 4, f);
};
const uint8_t sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
std::fwrite(sig, 1, 8, f);
std::vector<uint8_t> ihdr = {uint8_t(img.width >> 24), uint8_t(img.width >> 16), uint8_t(img.width >> 8), uint8_t(img.width),
uint8_t(img.height >> 24), uint8_t(img.height >> 16), uint8_t(img.height >> 8), uint8_t(img.height), 8, 6, 0, 0, 0};
chunk("IHDR", ihdr);
chunk("IDAT", z);
chunk("IEND", {});
return std::fclose(f) == 0;
}
// ------------------------------------------------------------------------------ bitmap font
// Classic 5x7 font, ASCII 32..126, column-major, bit 0 = top row.
static const uint8_t kFont5x7[95][5] = {
{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14},
{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},
{0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x08,0x2A,0x1C,0x2A,0x08},{0x08,0x08,0x3E,0x08,0x08},
{0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},{0x20,0x10,0x08,0x04,0x02},
{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},
{0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},
{0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00},{0x00,0x56,0x36,0x00,0x00},
{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},
{0x32,0x49,0x79,0x41,0x3E},{0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
{0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x01,0x01},{0x3E,0x41,0x41,0x51,0x32},
{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},
{0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x04,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x46,0x49,0x49,0x49,0x31},
{0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x7F,0x20,0x18,0x20,0x7F},
{0x63,0x14,0x08,0x14,0x63},{0x03,0x04,0x78,0x04,0x03},{0x61,0x51,0x49,0x45,0x43},{0x00,0x00,0x7F,0x41,0x41},
{0x02,0x04,0x08,0x10,0x20},{0x41,0x41,0x7F,0x00,0x00},{0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40},
{0x00,0x01,0x02,0x04,0x00},{0x20,0x54,0x54,0x54,0x78},{0x7F,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},
{0x38,0x44,0x44,0x48,0x7F},{0x38,0x54,0x54,0x54,0x18},{0x08,0x7E,0x09,0x01,0x02},{0x08,0x14,0x54,0x54,0x3C},
{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x44,0x3D,0x00},{0x00,0x7F,0x10,0x28,0x44},
{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x18,0x04,0x78},{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},
{0x7C,0x14,0x14,0x14,0x08},{0x08,0x14,0x14,0x18,0x7C},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20},
{0x04,0x3F,0x44,0x40,0x20},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},{0x3C,0x40,0x30,0x40,0x3C},
{0x44,0x28,0x10,0x28,0x44},{0x0C,0x50,0x50,0x50,0x3C},{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},
{0x00,0x00,0x7F,0x00,0x00},{0x00,0x41,0x36,0x08,0x00},{0x08,0x08,0x2A,0x1C,0x08},
};
BitmapFont BitmapFont::build() {
BitmapFont f;
f.image = Image(16 * 6, 6 * 8);
for (int c = 0; c < 95; c++) {
int cx = (c % 16) * 6, cy = (c / 16) * 8;
for (int col = 0; col < 5; col++)
for (int row = 0; row < 7; row++)
if (kFont5x7[c][col] >> row & 1) {
uint8_t* p = f.image.at(cx + col, cy + row);
p[0] = p[1] = p[2] = p[3] = 255;
}
}
return f;
}
RectF BitmapFont::glyph(char c) const {
int i = (c < 32 || c > 126) ? '?' - 32 : c - 32;
return {float((i % 16) * 6), float((i / 16) * 8), 6, 8};
}
void BitmapFont::draw(Renderer2D& r, const std::string& text, Vec2 pos, float scale, Color color, int16_t layer) const {
float x = pos.x;
for (char c : text) {
if (c != ' ') r.screen_rect(texture, glyph(c), {x, pos.y, 6 * scale, 8 * scale}, color, layer);
x += 6 * scale;
}
}
} // namespace unify