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:
co-authored by
Claude Opus 5.5
parent
c5b709d38d
commit
01cc252a1d
@@ -88,7 +88,9 @@ def local_files(client, locale):
|
||||
path = Path(dirpath) / name
|
||||
files[path.relative_to(client).as_posix()] = path
|
||||
for path in client.iterdir():
|
||||
if path.is_file() and (path.name == "channel.inf" or (path.name.startswith("locale") and path.suffix == ".cfg")):
|
||||
# hd_ui.pak: the 2x UI textures of scripts/build_hd_ui.py, when built (src/host/texture_decode.cpp).
|
||||
if path.is_file() and (path.name in ("channel.inf", "hd_ui.pak")
|
||||
or (path.name.startswith("locale") and path.suffix == ".cfg")):
|
||||
files[path.name] = path
|
||||
if locale:
|
||||
cfg = client / f"locale_{locale}.cfg"
|
||||
|
||||
+9
-1
@@ -4130,7 +4130,15 @@
|
||||
"focus_pause_exit_cleanup": "PARTIAL"
|
||||
},
|
||||
"platform_adaptations": [
|
||||
"Godot mobile touch/joystick/pinch/landscape UI is a platform adaptation and has no direct Windows 40250 branch to compare 1:1."
|
||||
"Godot mobile touch/joystick/pinch/landscape UI is a platform adaptation and has no direct Windows 40250 branch to compare 1:1.",
|
||||
{
|
||||
"reference": "CGraphicImageTexture loads the UI images (d:/ymir work/ui/, icon/, locale/<locale>/ui) from the packs at their 1x size; 40250 draws them 1:1 on an 800x600+ desktop",
|
||||
"implementation": "src/host/vk_textures.cpp get_gpu_texture -> src/host/texture_decode.cpp read_hd_ui_texture: a UI texture present in hd_ui.pak (scripts/build_hd_ui.py, realesrgan-x4plus + back-projection, 2x) is uploaded in place of the pack file; MT_HD_UI=0 disables",
|
||||
"invariant": "only the GPU texel density changes: the engine's own decode, the image sizes, layout, quads and normalized UVs stay the 40250 1x ones; a name absent from the pak falls back to the pack",
|
||||
"tests": [
|
||||
"real server 192.168.21.203 shop + character window A/B 2026-09-30 (MT_HD_UI=0 vs 1, 600-high UI at ~1.7x): identical layout, icons and portrait sharper, board grain kept; 2406 textures, 135 MB pak"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidence": {
|
||||
"contract": "audit/contracts/ui.input-camera-mobile.md",
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Builds hd_ui.pak: the 40250 UI textures at twice their resolution, for src/host/hd_ui.cpp.
|
||||
|
||||
The phone lays the 40250 UI out 600 logical pixels high and magnifies it about 1.8x onto the screen, so
|
||||
the 1x art (32x32 icons, board frames) looks soft. This reads every UI image the client would load
|
||||
(d:/ymir work/ui/, icon/, locale/<locale>/) from the registered packs (first registered pack wins, as
|
||||
CEterPackManager), upscales it 4x with Real-ESRGAN (realesrgan-x4plus, ncnn-vulkan build) and
|
||||
Lanczos-reduces it to exactly 2x. The engine keeps the 1x size for layout and UVs; the host only
|
||||
samples finer texels, so nothing about the 40250 UI changes but the texel density.
|
||||
|
||||
scripts/build_hd_ui.py --esrgan DIR [--client build/zh-locale/Client] [--locale zh] [--out FILE]
|
||||
|
||||
DIR holds the realesrgan-ncnn-vulkan binary and its models/ (github.com/xinntao/Real-ESRGAN releases,
|
||||
v0.2.5.0). Neither the tool nor the pak enters git: the output is Ymir art (see .gitignore).
|
||||
|
||||
Grain: realesrgan-x4plus smooths the stone and paper fills of the boards; two rounds of back-projection
|
||||
(the source minus the result box-reduced to 1x, enlarged and added) put that texture back.
|
||||
|
||||
Edges: each image is padded before upscaling so the network sees context at the border. An image whose
|
||||
opposite edges continue each other (a tiling board fill) is padded by wrapping, so its repeats meet
|
||||
without a seam; others by reflection. Colours under fully transparent pixels are bled in from their
|
||||
opaque neighbours first, so the upscaled alpha edges carry no dark fringe.
|
||||
|
||||
Pak format (little endian): "MTHD", u32 version (1), u32 count, then count x {u16 name length, name
|
||||
bytes (the pack path, lowercase), u32 offset, u32 size}, then the PNG payloads at their offsets.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "tools/epk_scan"))
|
||||
sys.path.insert(0, str(REPO / ".agents/skills/metin2-40250-parity-audit/scripts"))
|
||||
import epk_scan # noqa: E402
|
||||
import refroot # noqa: E402
|
||||
|
||||
IMAGE_EXTS = (".tga", ".dds", ".png", ".jpg", ".bmp")
|
||||
MAX_SIDE = 1024 # larger images (none in the UI dirs today) are left at 1x
|
||||
PAD = 8 # source pixels of context around each image
|
||||
MODEL = "realesrgan-x4plus"
|
||||
BACK_PROJECT_ROUNDS = 2
|
||||
VERSION = 1
|
||||
|
||||
|
||||
def pack_entries(client: Path):
|
||||
"""{pack path: reader} for the first registered pack holding each name."""
|
||||
_, keys, order, packs, _ = epk_scan.load(client, refroot.reference_root())
|
||||
seen = {}
|
||||
for name, _folder in order:
|
||||
p = packs[name]
|
||||
if p["missing"]:
|
||||
continue
|
||||
for e in p["entries"]:
|
||||
seen.setdefault(e["name"], (p["epk"], e))
|
||||
return keys, seen
|
||||
|
||||
|
||||
def read_entry(keys, epk: Path, e: dict) -> bytes:
|
||||
with open(epk, "rb") as f:
|
||||
f.seek(e["pos"])
|
||||
stored = f.read(e["size"])
|
||||
if e["type"] == 0:
|
||||
return stored
|
||||
f.seek(e["pos"])
|
||||
obj = f.read(epk_scan.lz_object_len(stored[:16]))
|
||||
return epk_scan.lz_object(obj, keys["s_adwEterPackSecurityKey"] if e["type"] == 2 else None)
|
||||
|
||||
|
||||
def wanted(name: str, locale: str) -> bool:
|
||||
if not name.endswith(IMAGE_EXTS):
|
||||
return False
|
||||
return (name.startswith("d:/ymir work/ui/") or name.startswith("icon/")
|
||||
or name.startswith(f"locale/{locale}/"))
|
||||
|
||||
|
||||
def tiles(a: np.ndarray, axis: int) -> bool:
|
||||
"""Whether the image continues across the edges of `axis` (1: left/right, 0: top/bottom)."""
|
||||
if a.shape[axis] < 4 or a[..., 3].min() < 255:
|
||||
return False
|
||||
rgb = a[..., :3].astype(np.int32)
|
||||
first, last = np.take(rgb, 0, axis=axis), np.take(rgb, -1, axis=axis)
|
||||
wrap = np.abs(first - last).mean()
|
||||
inner = np.abs(np.diff(rgb, axis=axis)).mean()
|
||||
return wrap <= max(2.0, 1.5 * inner)
|
||||
|
||||
|
||||
def bleed(im: Image.Image) -> Image.Image:
|
||||
"""Fills the colour of fully transparent pixels from nearby opaque ones (premultiplied blur)."""
|
||||
a = np.asarray(im).astype(np.float32)
|
||||
alpha = a[..., 3:4] / 255.0
|
||||
if alpha.min() > 0:
|
||||
return im
|
||||
fill = a[..., :3].copy()
|
||||
for radius in (2, 6, 16, 48):
|
||||
pre = Image.fromarray(np.clip(a[..., :3] * alpha, 0, 255).astype(np.uint8))
|
||||
w = Image.fromarray((alpha[..., 0] * 255).astype(np.uint8))
|
||||
pre_b = np.asarray(pre.filter(ImageFilter.GaussianBlur(radius))).astype(np.float32)
|
||||
w_b = np.asarray(w.filter(ImageFilter.GaussianBlur(radius))).astype(np.float32)[..., None]
|
||||
ok = (w_b > 0.5) & (alpha == 0)
|
||||
est = pre_b / np.maximum(w_b / 255.0, 1e-6)
|
||||
fill = np.where(ok, est, fill)
|
||||
alpha_known = (alpha > 0) | ok
|
||||
if alpha_known.all():
|
||||
break
|
||||
out = a.copy()
|
||||
out[..., :3] = np.where(alpha == 0, fill, a[..., :3])
|
||||
return Image.fromarray(np.clip(out, 0, 255).astype(np.uint8), "RGBA")
|
||||
|
||||
|
||||
def prepare(job):
|
||||
"""Decodes one pack image into a padded RGBA PNG for the upscaler; returns its metadata or None."""
|
||||
idx, name, data, work = job
|
||||
try:
|
||||
im = Image.open(io.BytesIO(data))
|
||||
im.load()
|
||||
except Exception:
|
||||
return None
|
||||
im = im.convert("RGBA")
|
||||
w, h = im.size
|
||||
if max(w, h) > MAX_SIDE or min(w, h) < 2:
|
||||
return None
|
||||
a = np.asarray(im)
|
||||
tile_x, tile_y = tiles(a, 1), tiles(a, 0)
|
||||
src = np.asarray(bleed(im))
|
||||
px, py = min(PAD, w), min(PAD, h)
|
||||
src = np.pad(src, ((0, 0), (px, px), (0, 0)), mode="wrap" if tile_x else "reflect")
|
||||
src = np.pad(src, ((py, py), (0, 0), (0, 0)), mode="wrap" if tile_y else "reflect")
|
||||
Image.fromarray(src, "RGBA").save(work / "in" / f"{idx}.png", compress_level=1)
|
||||
return {"idx": idx, "name": name, "w": w, "h": h, "px": px, "py": py,
|
||||
"alpha": bool(a[..., 3].min() < 255)}
|
||||
|
||||
|
||||
def original(meta, work: Path) -> Image.Image:
|
||||
src = Image.open(work / "in" / f"{meta['idx']}.png").convert("RGBA")
|
||||
return src.crop((meta["px"], meta["py"], meta["px"] + meta["w"], meta["py"] + meta["h"]))
|
||||
|
||||
|
||||
def back_project(hd: Image.Image, src: Image.Image, rounds: int = BACK_PROJECT_ROUNDS) -> Image.Image:
|
||||
"""Adds back what the upscale lost at the source scale, so the result box-reduces to the source.
|
||||
|
||||
realesrgan-x4plus denoises, and the fine stone and paper grain of the 40250 boards is what it takes
|
||||
for noise: the frames come out sharp but the fills flat. The residual (source minus the result
|
||||
reduced to 1x), bicubic-enlarged and added, restores that grain while the network's edges stay."""
|
||||
a = np.asarray(hd).astype(np.float32)
|
||||
ref = np.asarray(src).astype(np.float32)
|
||||
for _ in range(rounds):
|
||||
down = np.asarray(Image.fromarray(np.clip(a + 0.5, 0, 255).astype(np.uint8), "RGBA").resize(src.size, Image.BOX))
|
||||
residual = ref - down.astype(np.float32)
|
||||
a += np.stack([np.asarray(Image.fromarray(residual[..., c]).resize(hd.size, Image.BICUBIC)) for c in range(4)], -1)
|
||||
return Image.fromarray(np.clip(a + 0.5, 0, 255).astype(np.uint8), "RGBA")
|
||||
|
||||
|
||||
def finish(job):
|
||||
"""Crops the padding off the 4x result and reduces it to 2x; returns (name, png bytes) or None."""
|
||||
meta, work = job
|
||||
path = work / "out" / f"{meta['idx']}.png"
|
||||
if not path.exists():
|
||||
return None
|
||||
up = Image.open(path).convert("RGBA")
|
||||
s = 4
|
||||
up = up.crop((meta["px"] * s, meta["py"] * s, (meta["px"] + meta["w"]) * s, (meta["py"] + meta["h"]) * s))
|
||||
hd = back_project(up.resize((meta["w"] * 2, meta["h"] * 2), Image.LANCZOS), original(meta, work))
|
||||
if not meta["alpha"]:
|
||||
hd.putalpha(255)
|
||||
buf = io.BytesIO()
|
||||
hd.save(buf, "PNG", compress_level=6)
|
||||
return meta["name"], buf.getvalue()
|
||||
|
||||
|
||||
def write_pak(out: Path, items: list[tuple[str, bytes]]) -> None:
|
||||
items = sorted(items)
|
||||
offset = 12 + sum(2 + len(n.encode("latin-1")) + 8 for n, _ in items)
|
||||
header = [b"MTHD", struct.pack("<II", VERSION, len(items))]
|
||||
entries = []
|
||||
for name, data in items:
|
||||
raw = name.encode("latin-1")
|
||||
entries.append(struct.pack("<H", len(raw)) + raw + struct.pack("<II", offset, len(data)))
|
||||
offset += len(data)
|
||||
tmp = out.with_suffix(".tmp")
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(b"".join(header) + b"".join(entries))
|
||||
for _, data in items:
|
||||
f.write(data)
|
||||
os.replace(tmp, out)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--esrgan", type=Path, default=os.environ.get("MT_ESRGAN"), required="MT_ESRGAN" not in os.environ,
|
||||
help="directory with realesrgan-ncnn-vulkan and models/ (or MT_ESRGAN)")
|
||||
ap.add_argument("--client", type=Path, default=REPO / "build/zh-locale/Client")
|
||||
ap.add_argument("--locale", default="zh")
|
||||
ap.add_argument("--out", type=Path, help="default: <client>/hd_ui.pak")
|
||||
ap.add_argument("--work", type=Path, help="keep intermediates here (default: a temporary directory)")
|
||||
ap.add_argument("--reuse", action="store_true",
|
||||
help="keep the upscaled images already in --work and only redo the 2x reduction and the pak")
|
||||
ap.add_argument("--limit", type=int, default=0, help="only the first N images (a quick look)")
|
||||
args = ap.parse_args()
|
||||
out = args.out or args.client / "hd_ui.pak"
|
||||
binary = args.esrgan / "realesrgan-ncnn-vulkan"
|
||||
if not binary.exists():
|
||||
sys.exit(f"{binary} not found")
|
||||
|
||||
keys, seen = pack_entries(args.client)
|
||||
names = sorted(n for n in seen if wanted(n, args.locale))
|
||||
if args.limit:
|
||||
names = names[:args.limit]
|
||||
print(f"{len(names)} UI images in {args.client}/pack")
|
||||
|
||||
if args.reuse and not (args.work and (args.work / "manifest.json").exists()):
|
||||
sys.exit("--reuse needs a --work directory from an earlier run")
|
||||
work = args.work or Path(tempfile.mkdtemp(prefix="hd_ui_"))
|
||||
try:
|
||||
if args.reuse:
|
||||
metas = json.loads((work / "manifest.json").read_text())
|
||||
else:
|
||||
for sub in ("in", "out"):
|
||||
shutil.rmtree(work / sub, ignore_errors=True)
|
||||
(work / sub).mkdir(parents=True)
|
||||
jobs = [(i, n, read_entry(keys, *seen[n]), work) for i, n in enumerate(names)]
|
||||
with ProcessPoolExecutor() as pool:
|
||||
metas = [m for m in pool.map(prepare, jobs, chunksize=16) if m]
|
||||
print(f"prepared {len(metas)} (skipped {len(names) - len(metas)} undecodable or oversized)")
|
||||
(work / "manifest.json").write_text(json.dumps(metas))
|
||||
subprocess.run([str(binary), "-i", str(work / "in"), "-o", str(work / "out"), "-n", MODEL, "-s", "4",
|
||||
"-f", "png"], cwd=args.esrgan, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
with ProcessPoolExecutor() as pool:
|
||||
items = [r for r in pool.map(finish, [(m, work) for m in metas], chunksize=16) if r]
|
||||
write_pak(out, items)
|
||||
print(f"wrote {out}: {len(items)} images, {out.stat().st_size / 2**20:.1f} MB")
|
||||
finally:
|
||||
if not args.work:
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user