diff --git a/CMakeLists.txt b/CMakeLists.txt index b185f52865..f0106f9e92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -828,6 +828,8 @@ add_library(Common STATIC Common/Net/WebsocketServer.h Common/Profiler/Profiler.cpp Common/Profiler/Profiler.h + Common/Render/AtlasGen.cpp + Common/Render/AtlasGen.h Common/Render/TextureAtlas.cpp Common/Render/TextureAtlas.h Common/Render/DrawBuffer.cpp diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj index 7bea24a69d..518217997b 100644 --- a/Common/Common.vcxproj +++ b/Common/Common.vcxproj @@ -463,6 +463,7 @@ + @@ -917,6 +918,7 @@ + diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters index 10c2cc8417..d384e88d47 100644 --- a/Common/Common.vcxproj.filters +++ b/Common/Common.vcxproj.filters @@ -689,6 +689,9 @@ Audio + + Render + @@ -1290,6 +1293,9 @@ ext\imgui + + Render + diff --git a/Common/Render/AtlasGen.cpp b/Common/Render/AtlasGen.cpp new file mode 100644 index 0000000000..071fefd607 --- /dev/null +++ b/Common/Render/AtlasGen.cpp @@ -0,0 +1,249 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/StringUtils.h" +#include "Common/Render/TextureAtlas.h" + +#include "Common/Data/Format/PNGLoad.h" +#include "Common/Data/Format/ZIMSave.h" + +#include "Common/Data/Encoding/Utf8.h" +#include "Common/Render/AtlasGen.h" + +using namespace std; + +typedef unsigned short u16; + +void Image::copyfrom(const Image &img, int ox, int oy, Effect effect) { + assert(img.width() + ox <= width()); + assert(img.height() + oy <= height()); + for (int y = 0; y < (int)img.height(); y++) { + for (int x = 0; x < (int)img.width(); x++) { + switch (effect) { + case Effect::FX_COPY: + set1(x + ox, y + oy, img.get1(x, y)); + break; + case Effect::FX_RED_TO_ALPHA_SOLID_WHITE: + set1(x + ox, y + oy, 0x00FFFFFF | (img.get1(x, y) << 24)); + break; + case Effect::FX_RED_TO_INTENSITY_ALPHA_255: + { + u32 val = img.get1(x, y) & 0xFF; + set1(x + ox, y + oy, 0xFF000000 | val | (val << 8) | (val << 16)); + break; + } + case Effect::FX_PREMULTIPLY_ALPHA: + { + unsigned int color = img.get1(x, y); + unsigned int a = color >> 24; + unsigned int r = (color & 0xFF) * a >> 8, g = (color & 0xFF00) * a >> 8, b = (color & 0xFF0000) * a >> 8; + color = (color & 0xFF000000) | (r & 0xFF) | (g & 0xFF00) | (b & 0xFF0000); + // Simulate 4444 + color = color & 0xF0F0F0F0; + color |= color >> 4; + set1(x + ox, y + oy, color); + break; + } + case Effect::FX_PINK_TO_ALPHA: + { + u32 val = img.get1(x, y); + set1(x + ox, y + oy, ((val & 0xFFFFFF) == 0xFF00FF) ? 0x00FFFFFF : (val | 0xFF000000)); + break; + } + default: + set1(x + ox, y + oy, 0xFFFF00FF); + break; + } + } + } +} + +void Image::set(int sx, int sy, int ex, int ey, u32 fil) { + for (int y = sy; y < ey; y++) { + for (int x = sx; x < ex; x++) { + dat[y * w + x] = fil; + } + } +} + +bool Image::LoadPNG(const char *png_name) { + unsigned char *img_data; + int w, h; + if (1 != pngLoad(png_name, &w, &h, &img_data)) { + printf("Failed to load %s\n", png_name); + return false; + } + resize(w, h); + for (int y = 0; y < h; y++) { + memcpy(dat.data() + y * w, img_data + 4 * y * w, 4 * w); + } + free(img_data); + return true; +} + +void Image::SavePNG(const char *png_name) { + // Save PNG + FILE *fil = fopen(png_name, "wb"); + png_structp png_ptr; + png_infop info_ptr; + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + assert(png_ptr); + info_ptr = png_create_info_struct(png_ptr); + assert(info_ptr); + png_init_io(png_ptr, fil); + //png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); + png_set_IHDR(png_ptr, info_ptr, w, h, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png_ptr, info_ptr); + for (int y = 0; y < height(); y++) { + png_write_row(png_ptr, (png_byte*)(dat.data() + y * w)); + } + png_write_end(png_ptr, NULL); + png_destroy_write_struct(&png_ptr, &info_ptr); +} + +void Image::SaveZIM(const char *zim_name, int zim_format) { + uint8_t *image_data = new uint8_t[width() * height() * 4]; + for (int y = 0; y < height(); y++) { + memcpy(image_data + y * width() * 4, (dat.data() + y * w), width() * 4); + } + FILE *f = fopen(zim_name, "wb"); + // SaveZIM takes ownership over image_data, there's no leak. + ::SaveZIM(f, width(), height(), width() * 4, zim_format | ZIM_DITHER, image_data); + fclose(f); +} + +std::vector Bucket::Resolve(int image_width, Image &dest) { + // Place all the little images - whatever they are. + // Uses greedy fill algorithm. Slow but works surprisingly well, CPUs are fast. + ImageU8 masq; + masq.resize(image_width, 1); + dest.resize(image_width, 1); + sort(items.begin(), items.end()); + for (int i = 0; i < (int)items.size(); i++) { + if ((i + 1) % 2000 == 0) { + printf("Resolving (%i / %i)\n", i, (int)items.size()); + } + int idx = (int)items[i].first.width(); + int idy = (int)items[i].first.height(); + if (idx > 1 && idy > 1) { + assert(idx <= image_width); + for (int ty = 0; ty < 2047; ty++) { + if (ty + idy + 1 > (int)dest.height()) { + // Every 16 lines of new space needed, grow the image. + masq.resize(image_width, ty + idy + 16); + dest.resize(image_width, ty + idy + 16); + } + // Brute force packing. + int sz = (int)items[i].first.width(); + auto &masq_ty = masq.dat[ty]; + auto &masq_idy = masq.dat[ty + idy - 1]; + for (int tx = 0; tx < image_width - sz; tx++) { + bool valid = !(masq_ty[tx] || masq_idy[tx] || masq_ty[tx + idx - 1] || masq_idy[tx + idx - 1]); + if (valid) { + for (int ity = 0; ity < idy && valid; ity++) { + for (int itx = 0; itx < idx && valid; itx++) { + if (masq.dat[ty + ity][tx + itx]) { + goto skip; + } + } + } + dest.copyfrom(items[i].first, tx, ty, (Effect)items[i].second.effect); + masq.set(tx, ty, tx + idx + 1, ty + idy + 1, 255); + + items[i].second.sx = tx; + items[i].second.sy = ty; + + items[i].second.ex = tx + idx; + items[i].second.ey = ty + idy; + + // printf("Placed %d at %dx%d-%dx%d\n", items[i].second.id, tx, ty, tx + idx, ty + idy); + goto found; + } + skip: + ; + } + } + found: + ; + } + } + + if ((int)dest.width() > image_width * 2) { + printf("PACKING FAIL : height=%i", (int)dest.width()); + exit(1); + } + + // Output the glyph data. + vector dats; + for (int i = 0; i < (int)items.size(); i++) + dats.push_back(items[i].second); + return dats; +} + +bool LoadImage(const char *imagefile, Effect effect, Bucket *bucket, int &global_id) { + Image img; + + bool success = false; + if (!strcmp(imagefile, "white.png")) { + img.resize(16, 16); + img.fill(0xFFFFFFFF); + success = true; + } else { + success = img.LoadPNG(imagefile); + // printf("loaded image: %ix%i\n", (int)img.dat[0].size(), (int)img.dat.size()); + } + if (!success) { + return false; + } + + Data dat; + memset(&dat, 0, sizeof(dat)); + dat.id = global_id++; + dat.sx = 0; + dat.sy = 0; + dat.ex = (int)img.width(); + dat.ey = (int)img.height(); + dat.effect = (int)effect; + bucket->AddItem(img, dat); + return true; +} + +AtlasImage ImageDesc::ToAtlasImage(float tw, float th, const vector &results) const { + AtlasImage img{}; + int i = result_index; + float toffx = 0.5f / tw; + float toffy = 0.5f / th; + img.u1 = results[i].sx / tw + toffx; + img.v1 = results[i].sy / th + toffy; + img.u2 = results[i].ex / tw - toffx; + img.v2 = results[i].ey / th - toffy; + img.w = results[i].ex - results[i].sx; + img.h = results[i].ey - results[i].sy; + truncate_cpy(img.name, name); + return img; +} + +void ImageDesc::OutputSelf(FILE *fil, float tw, float th, const vector &results) const { + int i = result_index; + float toffx = 0.5f / tw; + float toffy = 0.5f / th; + fprintf(fil, " {%ff, %ff, %ff, %ff, %d, %d, \"%s\"},\n", + results[i].sx / tw + toffx, + results[i].sy / th + toffy, + results[i].ex / tw - toffx, + results[i].ey / th - toffy, + results[i].ex - results[i].sx, + results[i].ey - results[i].sy, + name.c_str()); +} + +void ImageDesc::OutputHeader(FILE *fil, int index) const { + fprintf(fil, "#define %s %i\n", name.c_str(), index); +} diff --git a/Common/Render/AtlasGen.h b/Common/Render/AtlasGen.h new file mode 100644 index 0000000000..ce2db6503e --- /dev/null +++ b/Common/Render/AtlasGen.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +#include "Common/Render/TextureAtlas.h" + +constexpr int supersample = 16; +constexpr int distmult = 64 * 3; // this is "one pixel in the final version equals 64 difference". reduce this number to increase the "blur" radius, increase it to make things "sharper" +constexpr int maxsearch = (128 * supersample + distmult - 1) / distmult; + +enum class Effect { + FX_COPY = 0, + FX_RED_TO_ALPHA_SOLID_WHITE = 1, // for alpha fonts + FX_RED_TO_INTENSITY_ALPHA_255 = 2, + FX_PREMULTIPLY_ALPHA = 3, + FX_PINK_TO_ALPHA = 4, // for alpha fonts + FX_INVALID = 5, +}; + +struct ImageU8 { + std::vector> dat; + void resize(int x, int y) { + dat.resize(y); + for (int i = 0; i < y; i++) + dat[i].resize(x); + } + int width() const { + return (int)dat[0].size(); + } + int height() const { + return (int)dat.size(); + } + void set(int sx, int sy, int ex, int ey, unsigned char fil) { + for (int y = sy; y < ey; y++) + fill(dat[y].begin() + sx, dat[y].begin() + ex, fil); + } +}; + +struct Image { + int w; + int h; + + // WARNING: This only preserves data correctly if w stays the same. Which it does, in our application. + void resize(int x, int y) { + w = x; + h = y; + dat.resize(w * h); + } + int width() const { + return w; + } + int height() const { + return h; + } + void set1(int x, int y, u32 col) { + dat[y * w + x] = col; + } + void fill(u32 col) { + for (int i = 0; i < w * h; i++) { + dat[i] = col; + } + } + const u32 *data() const { + return dat.data(); + } + u32 get1(int x, int y) const { return dat[y * w + x]; } + void copyfrom(const Image &img, int ox, int oy, Effect effect); + void set(int sx, int sy, int ex, int ey, u32 fil); + bool LoadPNG(const char *png_name); + void SavePNG(const char *png_name); + void SaveZIM(const char *zim_name, int zim_format); +private: + std::vector dat; +}; + +struct Data { + // item ID + int id; + // dimensions of its spot in the world + int sx, sy, ex, ey; + // offset from the origin + float ox, oy; + float voffset; // to apply at the end + // distance to move the origin forward + float wx; + + int effect; + int charNum; +}; + +inline bool operator<(const Data &lhs, const Data &rhs) { + return lhs.id < rhs.id; // should be unique +} + +struct Bucket { + std::vector > items; + void AddItem(const Image &img, const Data &dat) { + items.push_back(std::make_pair(img, dat)); + } + std::vector Resolve(int image_width, Image &dest); +}; + +inline bool operator<(const Image &lhs, const Image &rhs) { + return lhs.width() * lhs.height() > rhs.width() * rhs.height(); +} + +struct ImageDesc { + std::string name; + std::string fileName; + Effect effect; + int result_index; + + AtlasImage ToAtlasImage(float tw, float th, const std::vector &results) const; + void OutputSelf(FILE *fil, float tw, float th, const std::vector &results) const; + void OutputHeader(FILE *fil, int index) const; +}; + +bool LoadImage(const char *imagefile, Effect effect, Bucket *bucket, int &global_id); diff --git a/Core/HW/StereoResampler.cpp b/Core/HW/StereoResampler.cpp index ec44ae1971..d7a621d118 100644 --- a/Core/HW/StereoResampler.cpp +++ b/Core/HW/StereoResampler.cpp @@ -164,6 +164,10 @@ void StereoResampler::Mix(s16 *samples, unsigned int numSamples, bool consider_f if (!samples) return; + if (!buffer_) { + return; + } + unsigned int currentSample; // Cache access in non-volatile variable diff --git a/Core/HW/StereoResampler.h b/Core/HW/StereoResampler.h index 137f7f1007..f12401715f 100644 --- a/Core/HW/StereoResampler.h +++ b/Core/HW/StereoResampler.h @@ -53,7 +53,7 @@ private: // This can be adjusted, for the case of non-60hz output (a few hz off). int inputSampleRateHz_ = 44100; - int16_t *buffer_; + int16_t *buffer_ = nullptr; std::atomic indexW_; std::atomic indexR_; float numLeftI_ = 0.0f; diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj index 46d2098b88..7825745660 100644 --- a/UWP/CommonUWP/CommonUWP.vcxproj +++ b/UWP/CommonUWP/CommonUWP.vcxproj @@ -161,6 +161,7 @@ + @@ -335,6 +336,7 @@ + diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters index 2da873848c..a6315e5801 100644 --- a/UWP/CommonUWP/CommonUWP.vcxproj.filters +++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters @@ -520,6 +520,9 @@ ext\imgui + + Render + @@ -999,6 +1002,9 @@ ext\imgui + + Render + diff --git a/Windows/WASAPIContext.cpp b/Windows/WASAPIContext.cpp index 1b622df3ba..368a2d2eeb 100644 --- a/Windows/WASAPIContext.cpp +++ b/Windows/WASAPIContext.cpp @@ -345,12 +345,14 @@ void WASAPIContext::AudioLoop() { if (framesToWrite > 0 && SUCCEEDED(renderClient_->GetBuffer(framesToWrite, &buffer))) { if (!tempBuf_) { // Mix directly to the output buffer, avoiding a copy. - callback_(reinterpret_cast(buffer), framesToWrite, format_->nSamplesPerSec, userdata_); + if (buffer) { + callback_(reinterpret_cast(buffer), framesToWrite, format_->nSamplesPerSec, userdata_); + } } else { // We decided previously that we need conversion, so mix to our temp buffer... callback_(tempBuf_, framesToWrite, format_->nSamplesPerSec, userdata_); // .. and convert according to format (we support multi-channel float and s16) - if (format == AudioFormat::S16) { + if (format == AudioFormat::S16 && buffer) { // Need to convert. s16 *dest = reinterpret_cast(buffer); for (UINT32 i = 0; i < framesToWrite; i++) { @@ -367,7 +369,7 @@ void WASAPIContext::AudioLoop() { } } } - } else if (format == AudioFormat::Float) { + } else if (format == AudioFormat::Float && buffer) { // We have a non-2 number of channels (since we're in the tempBuf_ 'if'), so we contract/expand. float *dest = reinterpret_cast(buffer); for (UINT32 i = 0; i < framesToWrite; i++) { diff --git a/android/jni/Android.mk b/android/jni/Android.mk index aa551d1152..29c5ea777c 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -324,6 +324,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Common/GPU/ShaderTranslation.cpp \ $(SRC)/Common/Render/ManagedTexture.cpp \ $(SRC)/Common/Render/DrawBuffer.cpp \ + $(SRC)/Common/Render/AtlasGen.cpp \ $(SRC)/Common/Render/TextureAtlas.cpp \ $(SRC)/Common/Render/Text/draw_text.cpp \ $(SRC)/Common/Render/Text/draw_text_android.cpp \ diff --git a/build_fontatlas.sh b/build_fontatlas.sh index bb5dbfd3ed..41fc74e330 100755 --- a/build_fontatlas.sh +++ b/build_fontatlas.sh @@ -1,2 +1,5 @@ -./ext/native/tools/build/atlastool font_atlasscript.txt font 8888 && mv font_atlas.zim font_atlas.meta assets && rm font_atlas.cpp font_atlas.h -./ext/native/tools/build/atlastool asciifont_atlasscript.txt asciifont 8888 && mv asciifont_atlas.zim asciifont_atlas.meta assets && rm asciifont_atlas.cpp asciifont_atlas.h +TOOL=./ext/native/tools/build/atlastool +# TOOL=Windows/ARM64/Debug/AtlasTool.exe + +$TOOL font_atlasscript.txt font 8888 && mv font_atlas.zim font_atlas.meta assets && rm font_atlas.cpp font_atlas.h +$TOOL asciifont_atlasscript.txt asciifont 8888 && mv asciifont_atlas.zim asciifont_atlas.meta assets && rm asciifont_atlas.cpp asciifont_atlas.h diff --git a/buildatlas.sh b/buildatlas.sh index 0142b9b0c0..e16c716fc1 100755 --- a/buildatlas.sh +++ b/buildatlas.sh @@ -1 +1,4 @@ -./ext/native/tools/build/atlastool ui_atlasscript.txt ui 8888 && mv ui_atlas.zim ui_atlas.meta assets && rm ui_atlas.cpp ui_atlas.h +TOOL=./ext/native/tools/build/atlastool +# TOOL=Windows/ARM64/Debug/AtlasTool.exe + +$TOOL ui_atlasscript.txt ui 8888 && mv ui_atlas.zim ui_atlas.meta assets && rm ui_atlas.cpp ui_atlas.h diff --git a/ext/native/tools/atlastool.cpp b/ext/native/tools/atlastool.cpp index 25b65e373e..a0571b5d11 100644 --- a/ext/native/tools/atlastool.cpp +++ b/ext/native/tools/atlastool.cpp @@ -18,25 +18,25 @@ // line height // dist-per-pixel +#include #include -#include -#include -#include +#include #include -#include #include -#include +#include #include -#include -#include +#include "ft2build.h" +#include "freetype/ftbitmap.h" +#include "zstd.h" -#include "Common/StringUtils.h" +#include "Common/Render/AtlasGen.h" #include "Common/Render/TextureAtlas.h" - -#include "Common/Data/Format/PNGLoad.h" +#include "Common/Data/Encoding/Utf8.h" +#include "Common/StringUtils.h" +#include "Common/CommonTypes.h" #include "Common/Data/Format/ZIMSave.h" - #include "kanjifilter.h" + // extracted only JIS Kanji on the CJK Unified Ideographs of UCS2. Cannot reading BlockAllocator. (texture size over) //#define USE_KANJI KANJI_STANDARD | KANJI_RARELY_USED | KANJI_LEVEL4 // daily-use character only. However, it is too enough this. @@ -48,27 +48,8 @@ // add kanjiFilter Array with KANJI_LEARNING_ORDER_ADDTIONAL. #define USE_KANJI KANJI_LEARNING_ORDER_ALL -#include "Common/Data/Encoding/Utf8.h" - using namespace std; -static int global_id; - -typedef unsigned short u16; - -struct CharRange : public AtlasCharRange { - std::set filter; -}; - -enum class Effect { - FX_COPY = 0, - FX_RED_TO_ALPHA_SOLID_WHITE = 1, // for alpha fonts - FX_RED_TO_INTENSITY_ALPHA_255 = 2, - FX_PREMULTIPLY_ALPHA = 3, - FX_PINK_TO_ALPHA = 4, // for alpha fonts - FX_INVALID = 5, -}; - static const char * const effect_str[5] = { "copy", "r2a", "r2i", "pre", "p2a", }; @@ -82,256 +63,46 @@ static Effect GetEffect(const char *text) { return Effect::FX_INVALID; } +struct CharRange : public AtlasCharRange { + std::set filter; +}; + +CharRange range(int start, int end, const std::set &filter) { + CharRange r; + r.start = start; + r.end = end + 1; + r.result_index = 0; + r.filter = filter; + return r; +} + +CharRange range(int start, int end) { + CharRange r; + r.start = start; + r.end = end + 1; + r.result_index = 0; + return r; +} + +inline bool operator <(const CharRange &a, const CharRange &b) { + // These ranges should never overlap so this should be enough. + return a.start < b.start; +} + + struct FontReference { - FontReference(string name, string file, vector ranges, int pixheight, float vertOffset) + FontReference(string name, string file, std::vector ranges, int pixheight, float vertOffset) : name_(name), file_(file), ranges_(ranges), size_(pixheight), vertOffset_(vertOffset) { } - string name_; - string file_; - vector ranges_; + std::string name_; + std::string file_; + std::vector ranges_; int size_; float vertOffset_; }; -typedef vector FontReferenceList; - -template -struct Image { - vector > dat; - void resize(int x, int y) { - dat.resize(y); - for (int i = 0; i < y; i++) - dat[i].resize(x); - } - int width() const { - return (int)dat[0].size(); - } - int height() const { - return (int)dat.size(); - } - void copyfrom(const Image &img, int ox, int oy, Effect effect) { - assert(img.dat[0].size() + ox <= dat[0].size()); - assert(img.dat.size() + oy <= dat.size()); - for (int y = 0; y < (int)img.dat.size(); y++) { - for (int x = 0; x < (int)img.dat[y].size(); x++) { - switch (effect) { - case Effect::FX_COPY: - dat[y + oy][ox + x] = img.dat[y][x]; - break; - case Effect::FX_RED_TO_ALPHA_SOLID_WHITE: - dat[y + oy][ox + x] = 0x00FFFFFF | (img.dat[y][x] << 24); - break; - case Effect::FX_RED_TO_INTENSITY_ALPHA_255: - dat[y + oy][ox + x] = 0xFF000000 | img.dat[y][x] | (img.dat[y][x] << 8) | (img.dat[y][x] << 16); - break; - case Effect::FX_PREMULTIPLY_ALPHA: - { - unsigned int color = img.dat[y][x]; - unsigned int a = color >> 24; - unsigned int r = (color & 0xFF) * a >> 8, g = (color & 0xFF00) * a >> 8, b = (color & 0xFF0000) * a >> 8; - color = (color & 0xFF000000) | (r & 0xFF) | (g & 0xFF00) | (b & 0xFF0000); - // Simulate 4444 - color = color & 0xF0F0F0F0; - color |= color >> 4; - dat[y + oy][ox + x] = color; - break; - } - case Effect::FX_PINK_TO_ALPHA: - dat[y + oy][ox + x] = ((img.dat[y][x] & 0xFFFFFF) == 0xFF00FF) ? 0x00FFFFFF : (img.dat[y][x] | 0xFF000000); - break; - default: - dat[y + oy][ox + x] = 0xFFFF00FF; - break; - } - } - } - } - void set(int sx, int sy, int ex, int ey, unsigned char fil) { - for (int y = sy; y < ey; y++) - fill(dat[y].begin() + sx, dat[y].begin() + ex, fil); - } - bool LoadPNG(const char *png_name) { - unsigned char *img_data; - int w, h; - if (1 != pngLoad(png_name, &w, &h, &img_data)) { - printf("Failed to load %s\n", png_name); - exit(1); - return false; - } - dat.resize(h); - for (int y = 0; y < h; y++) { - dat[y].resize(w); - memcpy(&dat[y][0], img_data + 4 * y * w, 4 * w); - } - free(img_data); - return true; - } - void SavePNG(const char *png_name) { - // Save PNG - FILE *fil = fopen(png_name, "wb"); - png_structp png_ptr; - png_infop info_ptr; - png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - assert(png_ptr); - info_ptr = png_create_info_struct(png_ptr); - assert(info_ptr); - png_init_io(png_ptr, fil); - //png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); - png_set_IHDR(png_ptr, info_ptr, (uint32_t)dat[0].size(), (uint32_t)dat.size(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - png_write_info(png_ptr, info_ptr); - for (int y = 0; y < (int)dat.size(); y++) { - png_write_row(png_ptr, (png_byte*)&dat[y][0]); - } - png_write_end(png_ptr, NULL); - png_destroy_write_struct(&png_ptr, &info_ptr); - } - void SaveZIM(const char *zim_name, int zim_format) { - uint8_t *image_data = new uint8_t[width() * height() * 4]; - for (int y = 0; y < height(); y++) { - memcpy(image_data + y * width() * 4, &dat[y][0], width() * 4); - } - FILE *f = fopen(zim_name, "wb"); - // SaveZIM takes ownership over image_data, there's no leak. - ::SaveZIM(f, width(), height(), width() * 4, zim_format | ZIM_DITHER, image_data); - fclose(f); - } -}; - -template -bool operator<(const Image &lhs, const Image &rhs) { - return lhs.dat.size() * lhs.dat[0].size() > rhs.dat.size() * rhs.dat[0].size(); -} - -struct Data { - // item ID - int id; - // dimensions of its spot in the world - int sx, sy, ex, ey; - // offset from the origin - float ox, oy; - float voffset; // to apply at the end - // distance to move the origin forward - float wx; - - int effect; - int charNum; -}; - -bool operator<(const Data &lhs, const Data &rhs) { - return lhs.id < rhs.id; // should be unique -} - -string out_prefix; - -int NextPowerOf2(int x) { - int powof2 = 1; - // Double powof2 until >= val - while (powof2 < x) powof2 <<= 1; - return powof2; -} - -struct Bucket { - vector, Data> > items; - void AddItem(const Image &img, const Data &dat) { - items.push_back(make_pair(img, dat)); - } - vector Resolve(int image_width, Image &dest) { - // Place all the little images - whatever they are. - // Uses greedy fill algorithm. Slow but works surprisingly well, CPUs are fast. - Image masq; - masq.resize(image_width, 1); - dest.resize(image_width, 1); - sort(items.begin(), items.end()); - for (int i = 0; i < (int)items.size(); i++) { - if ((i + 1) % 2000 == 0) { - printf("Resolving (%i / %i)\n", i, (int)items.size()); - } - int idx = (int)items[i].first.dat[0].size(); - int idy = (int)items[i].first.dat.size(); - if (idx > 1 && idy > 1) { - assert(idx <= image_width); - for (int ty = 0; ty < 2047; ty++) { - if (ty + idy + 1 > (int)dest.dat.size()) { - masq.resize(image_width, ty + idy + 16); - dest.resize(image_width, ty + idy + 16); - } - // Brute force packing. - int sz = (int)items[i].first.dat[0].size(); - auto &masq_ty = masq.dat[ty]; - auto &masq_idy = masq.dat[ty + idy - 1]; - for (int tx = 0; tx < image_width - sz; tx++) { - bool valid = !(masq_ty[tx] || masq_idy[tx] || masq_ty[tx + idx - 1] || masq_idy[tx + idx - 1]); - if (valid) { - for (int ity = 0; ity < idy && valid; ity++) { - for (int itx = 0; itx < idx && valid; itx++) { - if (masq.dat[ty + ity][tx + itx]) { - goto skip; - } - } - } - dest.copyfrom(items[i].first, tx, ty, (Effect)items[i].second.effect); - masq.set(tx, ty, tx + idx + 1, ty + idy + 1, 255); - - items[i].second.sx = tx; - items[i].second.sy = ty; - - items[i].second.ex = tx + idx; - items[i].second.ey = ty + idy; - - // printf("Placed %d at %dx%d-%dx%d\n", items[i].second.id, tx, ty, tx + idx, ty + idy); - goto found; - } - skip: - ; - } - } - found: - ; - } - } - - if ((int)dest.dat.size() > image_width * 2) { - printf("PACKING FAIL : height=%i", (int)dest.dat.size()); - exit(1); - } - dest.resize(image_width, (int)dest.dat.size()); - - // Output the glyph data. - vector dats; - for (int i = 0; i < (int)items.size(); i++) - dats.push_back(items[i].second); - return dats; - } -}; - -const int supersample = 16; -const int distmult = 64 * 3; // this is "one pixel in the final version equals 64 difference". reduce this number to increase the "blur" radius, increase it to make things "sharper" -const int maxsearch = (128 * supersample + distmult - 1) / distmult; - -struct Closest { - FT_Bitmap bmp; - Closest(FT_Bitmap bmp) : bmp(bmp) { } - float find_closest(int x, int y, char search) { - int best = 1 << 30; - for (int i = 1; i <= maxsearch; i++) { - if (i * i >= best) - break; - for (int f = -i; f < i; f++) { - int dist = i * i + f * f; - if (dist >= best) continue; - if (safe_access(x + i, y + f) == search || safe_access(x - f, y + i) == search || safe_access(x - i, y - f) == search || safe_access(x + f, y - i) == search) - best = dist; - } - } - return sqrt((float)best); - } - char safe_access(int x, int y) { - if (x < 0 || y < 0 || x >= (int)bmp.width || y >= (int)bmp.rows) - return 0; - return bmp.buffer[x + y * bmp.width]; - } -}; +typedef std::vector FontReferenceList; typedef vector FT_Face_List; @@ -355,7 +126,31 @@ inline vector merge(const vector &a, const vector &ranges, float *metrics_height, Bucket *bucket) { +struct Closest { + FT_Bitmap bmp; + Closest(FT_Bitmap bmp) : bmp(bmp) {} + float find_closest(int x, int y, char search) { + int best = 1 << 30; + for (int i = 1; i <= maxsearch; i++) { + if (i * i >= best) + break; + for (int f = -i; f < i; f++) { + int dist = i * i + f * f; + if (dist >= best) continue; + if (safe_access(x + i, y + f) == search || safe_access(x - f, y + i) == search || safe_access(x - i, y - f) == search || safe_access(x + f, y - i) == search) + best = dist; + } + } + return (float)sqrt((float)best); + } + char safe_access(int x, int y) { + if (x < 0 || y < 0 || x >= (int)bmp.width || y >= (int)bmp.rows) + return 0; + return bmp.buffer[x + y * bmp.width]; + } +}; + +void RasterizeFonts(const FontReferenceList &fontRefs, vector &ranges, float *metrics_height, Bucket *bucket, int &global_id) { FT_Library freetype = 0; if (FT_Init_FreeType(&freetype) != 0) { printf("ERROR: Failed to init freetype\n"); @@ -426,7 +221,7 @@ void RasterizeFonts(const FontReferenceList &fontRefs, vector &ranges missing_chars++; } - Image img; + Image img; if (!foundMatch || filtered || 0 != FT_Load_Char(font, kar, FT_LOAD_RENDER | FT_LOAD_MONOCHROME)) { img.resize(1, 1); Data dat; @@ -457,8 +252,8 @@ void RasterizeFonts(const FontReferenceList &fontRefs, vector &ranges // No resampling, just sets the size of the image. img.resize((tempbitmap.width + supersample - 1) / supersample + bord * 2, (tempbitmap.rows + supersample - 1) / supersample + bord * 2); - int lmx = (int)img.dat[0].size(); - int lmy = (int)img.dat.size(); + int lmx = (int)img.width(); + int lmy = (int)img.height(); // AA by finding distance to character. Probably a fairly decent approximation but why not do it right? for (int y = 0; y < lmy; y++) { @@ -472,12 +267,12 @@ void RasterizeFonts(const FontReferenceList &fontRefs, vector &ranges dist = -closest.find_closest(ctx, cty, 1); } dist = dist / supersample * distmult + 127.5f; - dist = floor(dist + 0.5f); - if (dist < 0) dist = 0; - if (dist > 255) dist = 255; + dist = floorf(dist + 0.5f); + if (dist < 0.0f) dist = 0.0f; + if (dist > 255.0f) dist = 255.0f; // Only set the red channel. We process when adding the image. - img.dat[y][x] = (unsigned char)dist; + img.set1(x, y, (u8)dist); } } FT_Bitmap_Done(freetype, &tempbitmap); @@ -491,8 +286,8 @@ void RasterizeFonts(const FontReferenceList &fontRefs, vector &ranges dat.sx = 0; dat.sy = 0; - dat.ex = (int)img.dat[0].size(); - dat.ey = (int)img.dat.size(); + dat.ex = (int)img.width(); + dat.ey = (int)img.height(); dat.ox = (float)font->glyph->metrics.horiBearingX / 64 / supersample - bord; dat.oy = -(float)font->glyph->metrics.horiBearingY / 64 / supersample - bord; dat.voffset = vertOffset; @@ -514,39 +309,6 @@ void RasterizeFonts(const FontReferenceList &fontRefs, vector &ranges FT_Done_FreeType(freetype); } -bool LoadImage(const char *imagefile, Effect effect, Bucket *bucket) { - Image img; - - bool success = false; - if (!strcmp(imagefile, "white.png")) { - img.dat.resize(16); - for (int i = 0; i < 16; i++) { - img.dat[i].resize(16); - for (int j = 0; j < 16; j++) { - img.dat[i][j] = 0xFFFFFFFF; - } - } - success = true; - } else { - success = img.LoadPNG(imagefile); - // printf("loaded image: %ix%i\n", (int)img.dat[0].size(), (int)img.dat.size()); - } - if (!success) { - return false; - } - - Data dat; - memset(&dat, 0, sizeof(dat)); - dat.id = global_id++; - dat.sx = 0; - dat.sy = 0; - dat.ex = (int)img.dat[0].size(); - dat.ey = (int)img.dat.size(); - dat.effect = (int)effect; - bucket->AddItem(img, dat); - return true; -} - // Use the result array, and recorded data, to generate C++ tables for everything. struct FontDesc { string name; @@ -684,67 +446,6 @@ struct FontDesc { } }; -struct ImageDesc { - string name; - Effect effect; - int result_index; - - AtlasImage ToAtlasImage(float tw, float th, const vector &results) { - AtlasImage img{}; - int i = result_index; - float toffx = 0.5f / tw; - float toffy = 0.5f / th; - img.u1 = results[i].sx / tw + toffx; - img.v1 = results[i].sy / th + toffy; - img.u2 = results[i].ex / tw - toffx; - img.v2 = results[i].ey / th - toffy; - img.w = results[i].ex - results[i].sx; - img.h = results[i].ey - results[i].sy; - truncate_cpy(img.name, name); - return img; - } - - void OutputSelf(FILE *fil, float tw, float th, const vector &results) { - int i = result_index; - float toffx = 0.5f / tw; - float toffy = 0.5f / th; - fprintf(fil, " {%ff, %ff, %ff, %ff, %d, %d, \"%s\"},\n", - results[i].sx / tw + toffx, - results[i].sy / th + toffy, - results[i].ex / tw - toffx, - results[i].ey / th - toffy, - results[i].ex - results[i].sx, - results[i].ey - results[i].sy, - name.c_str()); - } - - void OutputHeader(FILE *fil, int index) { - fprintf(fil, "#define %s %i\n", name.c_str(), index); - } -}; - -CharRange range(int start, int end, const std::set &filter) { - CharRange r; - r.start = start; - r.end = end + 1; - r.result_index = 0; - r.filter = filter; - return r; -} - -CharRange range(int start, int end) { - CharRange r; - r.start = start; - r.end = end + 1; - r.result_index = 0; - return r; -} - -inline bool operator <(const CharRange &a, const CharRange &b) { - // These ranges should never overlap so this should be enough. - return a.start < b.start; -} - void LearnFile(const char *filename, const char *desc, std::set &chars, uint32_t lowerLimit, uint32_t upperLimit) { FILE *f = fopen(filename, "rb"); @@ -854,7 +555,7 @@ void GetLocales(const char *locales, std::vector &ranges) break; case 'J': // Shift JIS (for Japanese fonts) ranges.push_back(range(0x2010, 0x2312)); // General Punctuation, Letterlike Symbols, Arrows, - // Mathematical Operators, Miscellaneous Technical + // Mathematical Operators, Miscellaneous Technical ranges.push_back(range(0x2500, 0x254B)); // Box drawing ranges.push_back(range(0x25A0, 0x266F)); // Geometric Shapes, Miscellaneous Symbols break; @@ -903,38 +604,22 @@ static bool WriteCompressed(const void *src, size_t sz, size_t num, FILE *fp) { return true; } -int main(int argc, char **argv) { - // initProgram(&argc, const_cast(&argv)); - // /usr/share/fonts/truetype/msttcorefonts/Arial_Black.ttf - // /usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-R.ttf - if (argc < 3) { - printf("Not enough arguments.\nSee buildatlas.sh for example.\n"); - return 1; - } - assert(argc >= 3); - - bool highcolor = false; - - if (argc > 3) { - if (!strcmp(argv[3], "8888")) { - highcolor = true; - printf("RGBA8888 enabled!\n"); - } - } - printf("Reading script %s\n", argv[1]); - const char *atlas_name = argv[2]; - string image_name = string(atlas_name) + "_atlas.zim"; - string meta_name = string(atlas_name) + "_atlas.meta"; - out_prefix = argv[2]; - +//argv[1], argv[2], argv[3] +int GenerateFromScript(const char *script_file, const char *atlas_name, bool highcolor) { map fontRefs; vector fonts; vector images; - Bucket bucket; + int global_id = 0; + + + std::string image_name = string(atlas_name) + "_atlas.zim"; + std::string meta_name = string(atlas_name) + "_atlas.meta"; + + const std::string out_prefix = atlas_name; char line[512]{}; - FILE *script = fopen(argv[1], "r"); + FILE *script = fopen(script_file, "r"); if (!fgets(line, 512, script)) { printf("Error fgets-ing\n"); } @@ -975,27 +660,34 @@ int main(int argc, char **argv) { Effect effect = GetEffect(effectname); printf("Image %s with effect %s (%i)\n", imagefile, effectname, (int)effect); ImageDesc desc; + desc.fileName = imagefile; desc.name = imagename; desc.effect = effect; - desc.result_index = (int)bucket.items.size(); + desc.result_index = 0; images.push_back(desc); - if (!LoadImage(imagefile, effect, &bucket)) { - fprintf(stderr, "Failed to load image %s\n", imagefile); - } } else { fprintf(stderr, "Warning: Failed to parse line starting with %s\n", line); } } fclose(script); - // Script fully read, now rasterize the fonts. + Bucket bucket; + + // Script fully read, now read images and rasterize the fonts. + for (auto &image : images) { + image.result_index = (int)bucket.items.size(); + if (!LoadImage(image.fileName.c_str(), image.effect, &bucket, global_id)) { + fprintf(stderr, "Failed to load image %s\n", image.fileName.c_str()); + } + } + for (auto it = fontRefs.begin(), end = fontRefs.end(); it != end; ++it) { FontDesc fnt; fnt.first_char_id = (int)bucket.items.size(); vector finalRanges; float metrics_height; - RasterizeFonts(it->second, finalRanges, &metrics_height, &bucket); + RasterizeFonts(it->second, finalRanges, &metrics_height, &bucket, global_id); printf("font rasterized.\n"); fnt.ranges = finalRanges; @@ -1008,11 +700,11 @@ int main(int argc, char **argv) { // Script read, all subimages have been generated. // Place the subimages onto the main texture. Also writes to png. - Image dest; + Image dest; // Place things on the bitmap. printf("Resolving...\n"); - vector results = bucket.Resolve(image_width, dest); + std::vector results = bucket.Resolve(image_width, dest); if (highcolor) { printf("Writing .ZIM %ix%i RGBA8888...\n", dest.width(), dest.height()); dest.SaveZIM(image_name.c_str(), ZIM_RGBA8888 | ZIM_ZSTD_COMPRESSED); @@ -1059,7 +751,7 @@ int main(int argc, char **argv) { } FILE *cpp_file = fopen((out_prefix + "_atlas.cpp").c_str(), "wb"); - fprintf(cpp_file, "// C++ generated by atlastool from %s (hrydgard@gmail.com)\n\n", argv[1]); + fprintf(cpp_file, "// C++ generated by atlastool from %s (hrydgard@gmail.com)\n\n", script_file); fprintf(cpp_file, "#include \"%s\"\n\n", (out_prefix + "_atlas.h").c_str()); for (int i = 0; i < (int)fonts.size(); i++) { FontDesc &xfont = fonts[i]; @@ -1100,7 +792,7 @@ int main(int argc, char **argv) { fclose(cpp_file); FILE *h_file = fopen((out_prefix + "_atlas.h").c_str(), "wb"); - fprintf(h_file, "// Header generated by atlastool from %s (hrydgard@gmail.com)\n\n", argv[1]); + fprintf(h_file, "// Header generated by atlastool from %s (hrydgard@gmail.com)\n\n", script_file); fprintf(h_file, "#pragma once\n"); fprintf(h_file, "#include \"gfx/texture_atlas.h\"\n\n"); if (fonts.size()) { @@ -1120,4 +812,28 @@ int main(int argc, char **argv) { fprintf(h_file, "extern const Atlas %s_atlas;\n", atlas_name); fprintf(h_file, "extern const AtlasImage %s_images[%i];\n", atlas_name, (int)images.size()); fclose(h_file); + return 0; +} + +int main(int argc, char **argv) { + // initProgram(&argc, const_cast(&argv)); + // /usr/share/fonts/truetype/msttcorefonts/Arial_Black.ttf + // /usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-R.ttf + if (argc < 3) { + printf("Not enough arguments.\nSee buildatlas.sh for example.\n"); + return 1; + } + assert(argc >= 3); + + bool highcolor = false; + + if (argc > 3) { + if (!strcmp(argv[3], "8888")) { + highcolor = true; + printf("RGBA8888 enabled!\n"); + } + } + printf("Reading script %s\n", argv[1]); + + return GenerateFromScript(argv[1], argv[2], highcolor); } diff --git a/font_atlasscript.txt b/font_atlasscript.txt index 3bffccd33d..fdae07e372 100644 --- a/font_atlasscript.txt +++ b/font_atlasscript.txt @@ -1,3 +1,3 @@ 2048 font UBUNTU24 assets/Roboto-Condensed.ttf UWERS 34 -2 -font UBUNTU24 C:/Windows/Fonts/ARIALUNI.ttf UWEhkcRGHKVTeS 30 0 +font UBUNTU24 C:/dev/ARIALUNI.ttf UWEhkcRGHKVTeS 30 0