Host: 2x UI textures from hd_ui.pak (AI-upscaled 40250 UI art)

The phone magnifies the 600-high 40250 UI about 1.8x, so the 1x window art
and icons look soft next to the oversampled text. scripts/build_hd_ui.py
upscales every UI image the zh client loads (d:/ymir work/ui/, icon/,
locale/zh/) with realesrgan-x4plus, reduces it to 2x and back-projects the
residual so the board grain the network denoises away comes back; the host
uploads a texture found in hd_ui.pak in place of the pack file. Layout,
sizes and UVs stay the engine's 1x ones. MT_HD_UI=0 disables it; the pak
(135 MB, Ymir art) is built into the client dir and synced to the phone,
never committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 20:04:46 +09:00
co-authored by Claude Opus 5.5
parent c5b709d38d
commit 01cc252a1d
6 changed files with 354 additions and 9 deletions
+80 -6
View File
@@ -7,8 +7,13 @@
#include "stb_image.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <stdexcept>
#include <unordered_map>
#include <utility>
namespace mt_host {
@@ -124,9 +129,11 @@ mtimage::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size)
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
bool read_live_pack_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes) {
if (vpath.empty() || !mtpack40250::ready())
return false;
namespace {
// The spellings a 40250 texture name may be registered under in the packs (CEterPackManager::Get
// accepts "d:/ymir work/..." and "ymir work/..." alike, in any case).
std::vector<std::string> pack_name_candidates(const std::string& vpath) {
std::string norm = vpath;
for (char& ch : norm)
if (ch == '\\') ch = '/';
@@ -141,14 +148,81 @@ bool read_live_pack_texture(const std::string& vpath, std::vector<std::uint8_t>&
ch = static_cast<char>(ch - 'A' + 'a');
return s;
};
for (const std::string& candidate : {
norm, stripped, "d:/" + stripped,
lower(norm), lower(stripped), lower("d:/" + stripped)}) {
return {norm, stripped, "d:/" + stripped, lower(norm), lower(stripped), lower("d:/" + stripped)};
}
// hd_ui.pak (scripts/build_hd_ui.py): "MTHD", u32 version 1, u32 count, count x {u16 name length,
// name, u32 offset, u32 size}, then PNG payloads. Names are lowercase pack paths.
struct HdUiPak {
std::FILE* file = nullptr;
std::unordered_map<std::string, std::pair<std::uint32_t, std::uint32_t>> entries;
};
std::uint32_t read_u32(const std::uint8_t* p) {
return std::uint32_t(p[0]) | (std::uint32_t(p[1]) << 8) | (std::uint32_t(p[2]) << 16) | (std::uint32_t(p[3]) << 24);
}
HdUiPak* open_hd_ui_pak() {
if (const char* env = std::getenv("MT_HD_UI"); env && std::strcmp(env, "0") == 0)
return nullptr;
const char* env_path = std::getenv("MT_HD_UI_PAK");
std::FILE* f = std::fopen(env_path && *env_path ? env_path : "hd_ui.pak", "rb");
if (!f)
return nullptr;
auto* pak = new HdUiPak;
pak->file = f;
std::uint8_t header[12];
if (std::fread(header, 1, 12, f) != 12 || std::memcmp(header, "MTHD", 4) != 0 || read_u32(header + 4) != 1) {
std::fprintf(stderr, "[hd_ui] hd_ui.pak: bad header, ignored\n");
std::fclose(f);
delete pak;
return nullptr;
}
const std::uint32_t count = read_u32(header + 8);
pak->entries.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
std::uint8_t len[2];
if (std::fread(len, 1, 2, f) != 2)
break;
std::string name(std::size_t(len[0] | (len[1] << 8)), '\0');
std::uint8_t pos[8];
if (std::fread(name.data(), 1, name.size(), f) != name.size() || std::fread(pos, 1, 8, f) != 8)
break;
pak->entries.emplace(std::move(name), std::make_pair(read_u32(pos), read_u32(pos + 4)));
}
std::fprintf(stderr, "[hd_ui] hd_ui.pak: %zu UI textures at 2x\n", pak->entries.size());
return pak;
}
} // namespace
bool read_live_pack_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes) {
if (vpath.empty() || !mtpack40250::ready())
return false;
for (const std::string& candidate : pack_name_candidates(vpath)) {
if (mtpack40250::read(candidate, bytes) && !bytes.empty())
return true;
}
return false;
}
bool read_hd_ui_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes) {
// Opened on first use, after live_client has made the client directory the working directory.
static HdUiPak* pak = open_hd_ui_pak();
static std::mutex mutex;
if (!pak || vpath.empty())
return false;
for (const std::string& candidate : pack_name_candidates(vpath)) {
auto it = pak->entries.find(candidate);
if (it == pak->entries.end())
continue;
std::lock_guard<std::mutex> lock(mutex);
bytes.resize(it->second.second);
return std::fseek(pak->file, long(it->second.first), SEEK_SET) == 0 &&
std::fread(bytes.data(), 1, bytes.size(), pak->file) == bytes.size();
}
return false;
}
#endif
} // namespace mt_host
+3
View File
@@ -13,6 +13,9 @@ mtimage::Image decode_tga(const std::uint8_t* data, std::size_t size);
mtimage::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size);
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
bool read_live_pack_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes);
// The 2x replacement of a UI texture from hd_ui.pak in the client directory (MT_HD_UI_PAK overrides
// the path, MT_HD_UI=0 disables it); false when there is none.
bool read_hd_ui_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes);
#endif
} // namespace mt_host
+7 -1
View File
@@ -105,8 +105,14 @@ VulkanWindow::GpuTexture& VulkanWindow::get_gpu_texture(
decoded = decode_texture_bytes(mtra.data(), mtra.size());
}
} else {
// PORT: the phone magnifies the 600-high 40250 UI about 1.8x, so the 1x window art and icons
// look soft. A UI texture found in hd_ui.pak (scripts/build_hd_ui.py) is uploaded at 2x in
// place of the pack file. The engine keeps the 1x size from its own decode for layout and
// quads, and the UVs are normalized, so only the texel density changes.
std::vector<std::uint8_t> pack_bytes;
if (read_live_pack_texture(texture_name, pack_bytes))
if (read_hd_ui_texture(texture_name, pack_bytes))
decoded = decode_texture_bytes(pack_bytes.data(), pack_bytes.size());
if (!decoded.ok() && read_live_pack_texture(texture_name, pack_bytes))
decoded = decode_texture_bytes(pack_bytes.data(), pack_bytes.size());
}
}