Files
Toastconcern--FrameStudio/src/tex_mips.h
T
ToastconcernandClaude Opus 5.5 f7345a5ac3 Frame Studio and Frame Home: cross-platform builds
Frame Studio now builds for Windows (x64, x86, ARM64), Linux (x64, x86, ARM64)
and macOS (Apple Silicon, Intel):
- studio/platform.h with Win32 and POSIX implementations (dialogs through
  tinyfiledialogs on Linux/macOS), a GLFW window beside the Win32 one, and a
  platform-neutral frame pacer
- MoltenVK support in the renderer (portability enumeration) and bundled in
  the macOS app
- 32-bit builds: large-address-aware, LZMA packing within a 32-bit address space
- unaligned file reads in the loaders go through loadUnaligned (no UB)
- scripts/build-studio.sh, studio/build_windows.bat [x64|x86|arm64],
  cmake/llvm-mingw.cmake, and a GitHub Actions workflow building every target

Frame Home installs without Studio: frame/install-frame-home.{sh,ps1} and
"Install Frame Home.bat" (packaged as FrameHome-installer.zip).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:03:25 -05:00

90 lines
4.7 KiB
C++

#pragma once
// Mip chains for textures that ship without one. Several Meta homes (Desert, Cascadia) carry 2048-8192 ASTC textures
// with only their full-size level: drawn as they are they shimmer at a distance, so the renderer decodes them at load,
// shrinks them to 2048 and builds the smaller levels in RGBA: blurrier than the original, slow to load, heavy in memory.
// Baking a .fenv gives them a real chain instead: the full-size level stays exactly as shipped, and each smaller one is
// box-filtered from the level above it and encoded to ASTC in the same block size. The GPU then samples them natively,
// full sharpness up close and no shimmer far away.
#include "core/types.h"
#include "loaders/rendtxtr_parser.h"
#include "astcenc.h"
#include <algorithm>
#include <memory>
#include <thread>
#include <vector>
namespace texmips {
// Worth a chain: an LDR 2D texture bigger than 256 with no smaller levels.
inline bool wants(const TexSource& t) {
return !t.hdr && t.faces == 1 && t.mips <= 1 && std::max(t.w, t.h) > 256 && astcDimsOk(t.w, t.h, t.bw, t.bh) &&
t.astc.size() >= astcMip0Bytes(t.w, t.h, t.bw, t.bh);
}
// `t` with its full chain, or null when it can't be made (the texture is then kept as it was). threads: encoder
// threads (0 = every core).
inline std::shared_ptr<TexSource> withMips(const TexSource& t, unsigned threads = 0) {
if (!wants(t)) return nullptr;
std::vector<u8> level;
const u32 mip0 = astcMip0Bytes(t.w, t.h, t.bw, t.bh);
if (!astc::decodeASTC(t.astc.data(), mip0, t.w, t.h, t.bw, t.bh, level, false, 0)) return nullptr;
astcenc_config cfg = {};
if (astcenc_config_init(ASTCENC_PRF_LDR, t.bw, t.bh, 1, ASTCENC_PRE_FAST, 0, &cfg) != ASTCENC_SUCCESS) return nullptr;
unsigned n = threads ? threads : std::thread::hardware_concurrency();
n = std::clamp(n, 1u, 16u);
astcenc_context* ctx = nullptr;
if (astcenc_context_alloc(&cfg, n, &ctx, nullptr) != ASTCENC_SUCCESS) return nullptr;
auto out = std::make_shared<TexSource>(t);
out->astc.assign(t.astc.begin(), t.astc.begin() + mip0); // the full-size level, exactly as shipped
const u32 levels = astcFullMipCount(t.w, t.h);
u32 w = t.w, h = t.h;
bool ok = true;
const astcenc_swizzle swz = {ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A};
for (u32 l = 1; l < levels && ok; ++l) {
// 2x2 box filter (an odd edge folds its last row / column in)
const u32 nw = std::max(1u, w / 2), nh = std::max(1u, h / 2);
std::vector<u8> next(static_cast<size_t>(nw) * nh * 4);
for (u32 y = 0; y < nh; ++y)
for (u32 x = 0; x < nw; ++x) {
const u32 x0 = std::min(w - 1, x * 2), x1 = std::min(w - 1, x * 2 + 1), y0 = std::min(h - 1, y * 2), y1 = std::min(h - 1, y * 2 + 1);
for (int c = 0; c < 4; ++c) {
const unsigned s = level[(static_cast<size_t>(y0) * w + x0) * 4 + c] + level[(static_cast<size_t>(y0) * w + x1) * 4 + c] +
level[(static_cast<size_t>(y1) * w + x0) * 4 + c] + level[(static_cast<size_t>(y1) * w + x1) * 4 + c];
next[(static_cast<size_t>(y) * nw + x) * 4 + c] = static_cast<u8>((s + 2) / 4);
}
}
level.swap(next);
w = nw;
h = nh;
const size_t bytes = static_cast<size_t>((w + t.bw - 1) / t.bw) * ((h + t.bh - 1) / t.bh) * 16;
const size_t at = out->astc.size();
out->astc.resize(at + bytes);
void* px = level.data();
astcenc_image img = {};
img.dim_x = w; img.dim_y = h; img.dim_z = 1;
img.data_type = ASTCENC_TYPE_U8;
img.data = &px;
std::vector<astcenc_error> e(n, ASTCENC_SUCCESS);
if (n > 1 && static_cast<size_t>(w) * h >= 256 * 256) { // big levels on every core, the small ones on one
std::vector<std::thread> th;
for (unsigned i = 0; i < n; ++i) th.emplace_back([&, i] { e[i] = astcenc_compress_image(ctx, &img, &swz, out->astc.data() + at, bytes, i); });
for (auto& x : th) x.join();
} else {
// A context made for n threads waits for all n in compress: one thread uses a context of its own.
astcenc_context* one = nullptr;
if (astcenc_context_alloc(&cfg, 1, &one, nullptr) != ASTCENC_SUCCESS) { ok = false; break; }
e[0] = astcenc_compress_image(one, &img, &swz, out->astc.data() + at, bytes, 0);
astcenc_context_free(one);
}
for (astcenc_error x : e) ok = ok && x == ASTCENC_SUCCESS;
astcenc_compress_reset(ctx);
}
astcenc_context_free(ctx);
if (!ok) return nullptr;
out->mips = levels;
return out;
}
} // namespace texmips