asset compression
Some checks failed
CI / build-test-release (push) Failing after 15m11s

This commit is contained in:
Jorijn van der Graaf 2026-05-12 01:16:40 +02:00
commit 03717b5f33
9 changed files with 230 additions and 14 deletions

View file

@ -0,0 +1,55 @@
/*
Crafter® Build
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module Crafter.Build:Asset_impl;
import :Asset;
import std;
#ifdef CRAFTER_BUILD_HAS_ASSET
import Crafter.Asset;
import Crafter.Math;
#endif
namespace fs = std::filesystem;
namespace Crafter {
std::string CompressAsset(const fs::path& input, const fs::path& output) {
std::string ext = input.extension().string();
#ifdef CRAFTER_BUILD_HAS_ASSET
try {
if (ext == ".png") {
auto tex = TextureAsset<Vector<std::uint8_t, 4, 4>>::LoadPNG<std::uint8_t>(input);
tex.SaveCompressed(output);
} else if (ext == ".obj") {
auto mesh = MeshAsset<VertexNormalTangentUVPacked>::LoadOBJ(input);
mesh.SaveCompressed(output);
} else {
return std::format("{}: unsupported asset extension '{}'", input.string(), ext);
}
} catch (const std::exception& e) {
return std::format("{}: {}", input.string(), e.what());
}
return {};
#else
return std::format(
"{}: crafter-build was bootstrapped without Crafter.Asset linkage; "
"rebuild via `./bin/crafter-build` so the self-host pass picks up the "
"Crafter.Asset dep declared in project.cpp",
input.string());
#endif
}
}

View file

@ -23,6 +23,7 @@ import :Clang;
import :Platform;
import :Test;
import :Progress;
import :Asset;
namespace fs = std::filesystem;
using namespace Crafter;
@ -291,6 +292,38 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
});
}
// Asset compilation: each .png/.obj listed in cfg.assets is run through
// Crafter.Asset's library (linked into crafter-build directly — see
// :Asset_impl) to produce the SaveCompressed .ctex/.cmesh in outputDir.
// Skipped if the output is newer than the source. Parallel with shaders
// and module compiles; cancellation participates in the same flag.
auto assetOutput = [](const fs::path& asset) -> std::optional<fs::path> {
std::string ext = asset.extension().string();
if (ext == ".png") return fs::path(asset.filename()).replace_extension(".ctex");
if (ext == ".obj") return fs::path(asset.filename()).replace_extension(".cmesh");
return std::nullopt;
};
for (const fs::path& asset : config.assets) {
std::optional<fs::path> outName = assetOutput(asset);
if (!outName) {
buildCancelled.store(true);
buildError = std::format("{}: unsupported asset extension (expected .png or .obj)", asset.string());
break;
}
fs::path out = outputDir / *outName;
if (fs::exists(out) && fs::exists(asset) && fs::last_write_time(asset) <= fs::last_write_time(out)) continue;
threads.emplace_back([&asset, out = std::move(out), &buildError, &buildCancelled]() {
Progress::Task task(std::format("Compressing asset {}", asset.filename().string()));
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::string result = CompressAsset(asset, out);
if (result.empty()) return;
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
});
}
threads.emplace_back([&config, &outputDir, &buildCancelled, &buildError]() {
Progress::Task task(std::format("Copying files for {}", config.name));
if (buildCancelled.load(std::memory_order_relaxed)) return;
@ -432,16 +465,20 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
depThreads.reserve(config.dependencies.size());
std::atomic<bool> repack(false);
// -I propagation that's valid for both C and C++ compiles. Module-only
// bits (-fprebuilt-module-path) stay on `command` only.
std::string includeFlags;
{
std::unordered_set<Configuration*> seen;
std::function<void(Configuration*)> addFlags = [&](Configuration* dep) {
if (!seen.insert(dep).second) return;
for (const auto& entry : fs::recursive_directory_iterator(dep->path)) {
if (entry.is_directory() && entry.path().filename() == "include") {
command += " -I" + entry.path().string();
includeFlags += " -I" + entry.path().string();
}
}
command += std::format(" -I{} -fprebuilt-module-path={}", dep->path.string(), dep->PcmDir().string());
includeFlags += std::format(" -I{}", dep->path.string());
command += std::format(" -fprebuilt-module-path={}", dep->PcmDir().string());
for (Configuration* sub : dep->dependencies) {
addFlags(sub);
}
@ -450,6 +487,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
addFlags(dep);
}
}
command += includeFlags;
for(Configuration* dep : config.dependencies) {
depThreads.emplace_back([&, dep](){
@ -507,17 +545,25 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
});
}
// Defines belong on both C and C++ compiles so vendored C dependencies
// can see configuration-level macros consistently with module sources.
std::string defineFlags;
for(const Define& define : config.defines) {
if(define.value.empty()) {
command += std::format(" -D {}", define.name);
defineFlags += std::format(" -D {}", define.name);
} else {
command += std::format(" -D {}={}", define.name, define.value);
defineFlags += std::format(" -D {}={}", define.name, define.value);
}
}
command += defineFlags;
// Track caller-provided compileFlags separately so the .c compile can
// pick them up too (vendored C deps usually need -I from this set).
std::string userFlags;
for(const std::string& flag : config.compileFlags) {
command += " " + flag;
userFlags += " " + flag;
}
command += userFlags;
std::string cmakeBuildType;
@ -534,11 +580,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
const std::string objPath = (buildDir / cFile.filename()).string() + "_source.o";
const std::string srcPath = cFile.string() + ".c";
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config]() {
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config, &includeFlags, &defineFlags, &userFlags]() {
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::string result = RunCommand(std::format("clang {}.c --target={} -march={} -mtune={} -O3 -c -o {}_source.o", cFile.string(), config.target, config.march, config.mtune, (buildDir / cFile.filename()).string()));
std::string result = RunCommand(std::format("clang {}.c --target={} -march={} -mtune={} -O3 -c{}{}{} -o {}_source.o", cFile.string(), config.target, config.march, config.mtune, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
if (result.empty()) return;
bool expected = false;
@ -632,6 +678,16 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
if (!fs::exists(src)) continue;
copyTree(src, outputDir / additionalFile.filename());
}
for (const fs::path& asset : dep->assets) {
std::string ext = asset.extension().string();
fs::path srcName = asset.filename();
if (ext == ".png") srcName.replace_extension(".ctex");
else if (ext == ".obj") srcName.replace_extension(".cmesh");
else continue;
fs::path src = depBinDir / srcName;
if (!fs::exists(src)) continue;
copyTree(src, outputDir / srcName);
}
for (Configuration* sub : dep->dependencies) forwardDepArtifacts(sub);
};
for (Configuration* dep : config.dependencies) forwardDepArtifacts(dep);

View file

@ -227,7 +227,7 @@ fs::path Crafter::GetCacheDir() {
}
namespace {
constexpr std::array<std::string_view, 9> kCrafterBuildModules = {
constexpr std::array<std::string_view, 10> kCrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
@ -236,6 +236,7 @@ namespace {
"Crafter.Build-Clang",
"Crafter.Build-Test",
"Crafter.Build-Progress",
"Crafter.Build-Asset",
"Crafter.Build",
};
}
@ -737,7 +738,7 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
}
namespace {
constexpr std::array<std::string_view, 9> kCrafterBuildModules = {
constexpr std::array<std::string_view, 10> kCrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
@ -746,6 +747,7 @@ namespace {
"Crafter.Build-Clang",
"Crafter.Build-Test",
"Crafter.Build-Progress",
"Crafter.Build-Asset",
"Crafter.Build",
};