harfang3d/tools/assetc/assetc.cpp
2022-12-07 09:51:01 +01:00

2023 lines
62 KiB
C++

// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details.
#include <algorithm>
#include <deque>
#include <future>
#include <iostream>
#include <map>
#include <mutex>
#include <regex>
#include <set>
#include <string>
#include <thread>
#include <json/json.hpp>
#include <process.hpp>
#include <foundation/build_info.h>
#include <foundation/cmd_line.h>
#include <foundation/data.h>
#include <foundation/dir.h>
#include <foundation/file.h>
#include <foundation/format.h>
#include <foundation/log.h>
#include <foundation/path_tools.h>
#include <foundation/profiler.h>
#include <foundation/string.h>
#include <foundation/time.h>
#include <foundation/time_chrono.h>
#include <foundation/xxhash.h>
#include <engine/forward_pipeline.h>
#include <engine/geometry.h>
#include <engine/meta.h>
#include <engine/render_pipeline.h>
#include <engine/scene.h>
#include <platform/filesystem_watcher.h>
#include <platform/process.h>
#define HASH_METHOD 2 // 0:SHA1 1:Murmurv3 2:xxHash
#if USE_SHA1
#include <foundation/sha1.h>
#else
#include <foundation/murmur3.h>
#endif
#if WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <shellapi.h>
#undef CopyFile
#endif
using namespace hg;
namespace assetc {
static const int classify_progress_weight = 30;
static const int compile_progress_weight = 20;
static const int run_queue_progress_weight = 50;
//
static std::string cwd;
//
static std::atomic<int> error_count;
static bool log_errors_to_stderr = false;
static bool progress = false;
static std::mutex failed_inputs_mutex;
static std::set<std::string> failed_inputs;
static void ReportFailedInput(std::string name) {
std::lock_guard<std::mutex> lock(failed_inputs_mutex);
failed_inputs.insert(std::move(name));
}
void log_error(const json &js) {
std::stringstream ss;
if (log_errors_to_stderr) {
ss << js.dump(4) << std::endl;
std::cerr << ss.str();
} else {
const auto type = js["type"].get<std::string>();
ss << "Error " << type << std::endl;
if (type == "CompileProcessReturnedNonZero") {
ss << " -> CWD: " << js["cwd"].get<std::string>() << std::endl;
ss << " -> CMD: " << js["cmd"].get<std::string>() << std::endl;
const auto out = js["out"].get<std::string>();
if (!out.empty())
ss << " -> STDOUT:" << std::endl << out << std::endl;
const auto err = js["err"].get<std::string>();
if (!err.empty())
ss << " -> STDERR:" << std::endl << err << std::endl;
}
std::cout << ss.str();
}
++error_count;
}
//
static bool compile_with_debug_info = false;
//
static std::string global_shader_defines;
//
struct Toolchain {
std::string shaderc, texturec, luac, cmft, recastc, texconv, bulletc;
};
static Toolchain toolchain;
static void ToolchainExists() {
if (!IsFile(toolchain.shaderc.c_str()))
toolchain.shaderc.clear();
if (!IsFile(toolchain.texturec.c_str()))
toolchain.texturec.clear();
if (!IsFile(toolchain.luac.c_str()))
toolchain.luac.clear();
if (!IsFile(toolchain.cmft.c_str()))
toolchain.cmft.clear();
if (!IsFile(toolchain.recastc.c_str()))
toolchain.recastc.clear();
if (!IsFile(toolchain.texconv.c_str()))
toolchain.texconv.clear();
if (!IsFile(toolchain.bulletc.c_str()))
toolchain.bulletc.clear();
}
static void SetToolchain(const std::string &path) {
#ifdef WIN32
toolchain = {PathJoin({path, "shaderc.exe"}), PathJoin({path, "texturec.exe"}), PathJoin({path, "luac.exe"}), PathJoin({path, "cmft.exe"}),
PathJoin({path, "recastc.exe"}), PathJoin({path, "texconv.exe"}), PathJoin({path, "bulletc.exe"})};
#else
toolchain = {PathJoin({path, "shaderc"}), PathJoin({path, "texturec"}), PathJoin({path, "luac"}), PathJoin({path, "cmft"}), PathJoin({path, "recast"}), PathJoin({path, "texconv"}), PathJoin({path, "bulletc"})};
#endif
}
//
using process_id = unsigned long;
static process_id poll_process_id = 0;
static bool IsPollProcessUp() {
if (poll_process_id == 0)
return true;
bool running = true;
is_process_running(poll_process_id, running);
return running;
}
//
static std::string api = HG_GRAPHIC_API; // DX11/DX12/GL
#if defined(WIN32)
#define ASSSETC_DEFAULT_PLATFORM "windows"
#elif defined(__linux__)
#define ASSSETC_DEFAULT_PLATFORM "linux"
#elif defined(__APPLE__)
#define ASSSETC_DEFAULT_PLATFORM "osx"
#else
#define ASSSETC_DEFAULT_PLATFORM ""
#endif
// remaining shaderc platforms
// android
// asm.js
// ios
// orbis
static std::string platform = ASSSETC_DEFAULT_PLATFORM;
#if 0
// CWE 561: The function 'SetAPI' is never used.
void SetAPI(const char *api_) { api = api_; }
#endif
//
static std::string input_dir;
static std::string output_dir;
static std::string FullInputPath(const std::string &path) { return PathJoin({input_dir, path}); }
static std::string FullOutputPath(const std::string &path) { return PathJoin({output_dir, path}); }
static std::string FullOutputDir(const std::string &path) { return CutFileName(FullOutputPath(path)); }
static bool MkOutputTree(const std::string &path) {
const auto res = MkTree(FullOutputDir(path).c_str());
if (!res) {
const json json_err = {{"type", "FailedToMkOutputTree"}, {"path", path}};
log_error(json_err);
}
return res;
}
#if HASH_METHOD == 0
using Hash = SHA1Hash;
#else
using Hash = std::array<char, 16>; // 128 bit
#endif
void ComputeHash(const void *data, size_t len, Hash &out) {
ProfilerPerfSection perf("Manage/ComputeHash");
#if HASH_METHOD == 0
ComputeSHA1(data, len, out);
#elif HASH_METHOD == 1
#if defined(_M_X64) || defined(__amd64__)
MurmurHash3_x64_128(data, len, 0, &out);
#else
MurmurHash3_x86_128(data, len, 0, &out);
#endif
#else
XXH128(data, len, 0, &out);
#endif
}
//
struct CompilationDB {
std::map<std::string, std::set<std::string>> output_to_inputs;
std::map<std::string, Hash> source_hashes;
std::map<std::string, Hash> output_build_params;
};
static CompilationDB compilation_db;
static size_t processed_count = 0;
static bool fast_check = false;
static bool TestInputs(std::map<std::string, Hash> &hashes, const std::set<std::string> &inputs) {
bool need_refresh = false;
for (const auto &i : inputs) {
const auto input_path = FullInputPath(i);
if (!IsFile(input_path.c_str()))
continue;
Data data;
if (fast_check) {
const auto info = GetFileInfo(input_path.c_str());
if (info.size == 0)
error(format("Failed to stat input '%1'").arg(input_path));
Write(data, info.size);
Write(data, info.modified);
} else {
if (!LoadDataFromFile(input_path.c_str(), data))
error(format("Failed to load '%1' data").arg(input_path));
}
Hash hash;
ComputeHash(data.GetData(), data.GetSize(), hash);
const auto &j = compilation_db.source_hashes.find(i);
if (j == std::end(compilation_db.source_hashes)) {
debug(format(" [!] Source file '%1' hash missing, triggering refresh").arg(i));
need_refresh = true;
} else {
if (j->second != hash) {
debug(format(" [!] Source file '%1' hash mismatch, triggering refresh").arg(i));
need_refresh = true; // modified source
}
}
hashes[i] = hash; // update source file hash
}
return need_refresh;
}
static bool TestOutputs(const std::set<std::string> &outputs, const Data &build_context) {
bool need_refresh = false;
Hash build_params_hash{};
ComputeHash(build_context.GetData(), build_context.GetSize(), build_params_hash);
for (const auto &output : outputs) {
if (!IsFile(FullOutputPath(output).c_str())) {
debug(format(" [!] Output file '%1' missing, triggering refresh").arg(output));
need_refresh = true; // missing output asset
}
auto i = compilation_db.output_build_params.find(output);
if (i == std::end(compilation_db.output_build_params)) {
debug(format(" [!] Output file '%1' build context missing, triggering refresh").arg(output));
need_refresh = true;
} else {
if (i->second != build_params_hash) {
debug(format(" [!] Output file '%1' build context changed, triggering refresh").arg(output));
need_refresh = true; // build params modified
}
}
compilation_db.output_build_params[output] = build_params_hash; // update build params hash
}
return need_refresh;
}
static bool NeedsCompilation(
std::map<std::string, Hash> &hashes, const std::set<std::string> &inputs, const std::set<std::string> &outputs, const Data &build_context) {
ProfilerPerfSection perf("Manage/NeedsCompilation");
bool need_refresh = false;
if (TestOutputs(outputs, build_context))
need_refresh = true;
if (TestInputs(hashes, inputs))
need_refresh = true;
if (need_refresh)
processed_count += outputs.size();
for (const auto &output : outputs)
compilation_db.output_to_inputs[output] = inputs;
return need_refresh;
}
static void CleanOutputs(const std::set<std::string> &outputs) {
ProfilerPerfSection perf("Manage/CleanOutputs");
for (const auto &output : outputs)
Unlink(FullOutputPath(output).c_str());
}
//
void Reset() {
compilation_db.source_hashes.clear();
compilation_db.output_to_inputs.clear();
compilation_db.output_build_params.clear();
}
//
void SetInputDir(const std::string &path) { input_dir = CleanPath(path); }
bool SetOutputDir(const std::string &path) {
output_dir = CleanPath(path);
const auto res = MkTree(output_dir.c_str());
if (!res) {
const json json_err = {{"type", "SetOutputDirFailedToMkTree"}, {"path", output_dir}};
log_error(json_err);
}
return res;
}
//
static json LoadMeta(const std::string &path) {
ProfilerPerfSection perf("Manage/LoadMeta");
return LoadResourceMetaFromFile(FullInputPath(path));
}
//
static std::mutex default_log_mutex;
static std::map<std::string, std::string> default_log_output;
static std::map<std::string, std::string> default_error_log_output;
static void RunProcess(const std::string &name, const std::string &cmd, const std::string &cwd) {
//
const auto cmd_elms = split(cmd, " ");
ProfilerPerfSection perf(format("Command/RunProcess/%1").arg(cmd_elms[0]));
log(format(" Spawning compile process for %1").arg(name));
TinyProcessLib::Process process(
cmd, cwd,
[&](const char *bytes, size_t n) {
std::lock_guard<std::mutex> lock(default_log_mutex);
default_log_output[name] += {bytes, n};
},
[&](const char *bytes, size_t n) {
std::lock_guard<std::mutex> lock(default_log_mutex);
default_error_log_output[name] += {bytes, n};
},
false);
if (process.get_id() == 0) {
ReportFailedInput(name);
const json json_err = {{"type", "FailedToSpawnCompileProcess"}, {"name", name}};
log_error(json_err);
} else {
const auto res = process.get_exit_status();
std::string out, err;
{
std::lock_guard<std::mutex> lock(default_log_mutex);
{
auto i = default_log_output.find(name);
if (i != std::end(default_log_output)) {
out = std::move(i->second);
default_log_output.erase(i);
}
}
{
auto i = default_error_log_output.find(name);
if (i != std::end(default_error_log_output)) {
err = std::move(i->second);
default_error_log_output.erase(i);
}
}
}
if (res != 0) {
ReportFailedInput(name);
const json json_err = {{"type", "CompileProcessReturnedNonZero"}, {"res", res}, {"cmd", cmd}, {"cwd", cwd}, {"out", out}, {"err", err}};
log_error(json_err);
}
}
}
//
struct task {
std::string name;
std::function<std::future<void>()> func;
};
static std::deque<task> task_queue;
static void PushAsyncProcessTask(const std::string &name, const std::string &cmd, const std::string &cwd) {
task_queue.emplace_back(task{name, [=]() { return std::async(std::launch::async, RunProcess, name, cmd, cwd); }});
}
static int max_async_jobs = 0;
static void RunTaskQueue() {
const auto task_count = task_queue.size();
std::deque<std::future<void>> task_running;
std::string current_task_name;
while (!task_queue.empty() || !task_running.empty()) {
while (!task_queue.empty() && task_running.size() < max_async_jobs) {
current_task_name = task_queue.front().name;
task_running.emplace_back(task_queue.front().func()); // start task
task_queue.pop_front();
}
const auto task_done = task_count - (task_queue.size() + task_running.size());
if (progress)
std::cout << "-> Progress: " << task_done * run_queue_progress_weight / task_count + classify_progress_weight + compile_progress_weight << "% ("
<< current_task_name << ")" << std::endl;
time_ns t_ref = time_now();
while (!task_running.empty()) {
bool break_ = false;
for (auto &t : task_running)
if (t.wait_for(time_to_chrono(time_from_ms(1))) == std::future_status::ready) {
break_ = true;
break; // break asap to start a new task
}
if (break_)
break;
const auto t_now = time_now();
if (t_now - t_ref >= time_from_sec(5)) {
log(format(" %1 tasks running, %2 queued...").arg(task_running.size()).arg(task_queue.size()));
t_ref = t_now;
}
}
task_running.erase(std::remove_if(std::begin(task_running), std::end(task_running),
[](const std::future<void> &f) { return f.wait_for(time_to_chrono(0)) == std::future_status::ready; }),
std::end(task_running));
}
}
//
static const uint16_t CAB1 = 0xCAB1;
bool LoadCompilationDB(const char *filename) {
ProfilerPerfSection perf("Manage/LoadCompilationDB");
Reset();
if (!Exists(filename))
return false;
ScopedFile file(Open(filename));
if (!file)
return false;
//
if (Read<uint16_t>(file) != CAB1)
return false;
const auto version = Read<uint16_t>(file);
if (version > 2)
return false;
//
if (version >= 2) {
(void)ReadString(file); // build_sha
(void)ReadString(file); // version_string
}
//
const auto src_hashes_count = Read<uint32_t>(file);
for (uint32_t i = 0; i < src_hashes_count; ++i) {
const auto path = ReadString(file);
const auto hash = Read<Hash>(file);
compilation_db.source_hashes[path] = hash;
}
//
const auto asset_to_source_count = Read<uint32_t>(file);
for (uint32_t i = 0; i < asset_to_source_count; ++i) {
const auto name = ReadString(file);
const auto src_count = Read<uint32_t>(file);
for (uint32_t j = 0; j < src_count; ++j) {
const auto path = ReadString(file);
compilation_db.output_to_inputs[name].insert(path);
}
}
//
if (Tell(file) < GetSize(file)) {
const auto output_build_hash_count = Read<uint32_t>(file);
for (uint32_t i = 0; i < output_build_hash_count; ++i) {
const auto name = ReadString(file);
const auto hash = Read<Hash>(file);
compilation_db.output_build_params[name] = hash;
}
}
return true;
}
bool LoadCompilationDB() { return LoadCompilationDB(FullOutputPath("assetc.cab").c_str()); }
bool SaveCompilationDB(const char *path) {
ProfilerPerfSection perf("Manage/SaveCompilationDB");
std::cout << format(" %1 input files").arg(compilation_db.source_hashes.size()) << std::endl;
std::cout << format(" %1 output files").arg(compilation_db.output_to_inputs.size()) << std::endl;
std::cout << format(" %1 processed").arg(processed_count) << std::endl;
std::cout << format(" %1 failed").arg(error_count.load()) << std::endl;
for (const auto &i : failed_inputs)
std::cout << format(" - FAILED: %1").arg(i) << std::endl;
failed_inputs.clear();
std::cout << std::endl;
processed_count = 0;
error_count = 0;
std::cout << format(" Saving compilation DB '%1'").arg(path) << std::endl;
if (compilation_db.source_hashes.empty()) {
std::cout << " Empty DB, nothing to save" << std::endl;
return false; // nothing to save
}
//
ScopedFile file(OpenWrite(path));
if (!file) {
const json json_err = {{"type", "FailedToSaveCompilationDB"}, {"path", path}};
log_error(json_err);
return false;
}
/*
version 0: initial version
version 1: switch to murmurv3 hash
version 2: add build sha and version string
*/
Write(file, CAB1);
Write(file, uint16_t(2)); // version
WriteString(file, get_build_sha());
WriteString(file, get_version_string());
//
Write(file, uint32_t(compilation_db.source_hashes.size()));
for (const auto &i : compilation_db.source_hashes) {
WriteString(file, i.first);
Write(file, i.second);
}
//
Write(file, uint32_t(compilation_db.output_to_inputs.size()));
for (const auto &i : compilation_db.output_to_inputs) {
const auto &name = i.first;
WriteString(file, name);
const auto &sources = i.second;
Write(file, uint32_t(sources.size()));
for (auto &s : sources)
WriteString(file, s);
}
//
Write(file, uint32_t(compilation_db.output_build_params.size()));
for (const auto &i : compilation_db.output_build_params) {
WriteString(file, i.first);
Write(file, i.second);
}
return true;
}
bool SaveCompilationDB() { return SaveCompilationDB(FullOutputPath("assetc.cab").c_str()); }
//
static PipelineInfo pipeline;
static void SetRenderPipeline(const PipelineInfo &_pipeline) { pipeline = _pipeline; }
//
static void Copy(std::map<std::string, Hash> &hashes, const std::string &path) {
ProfilerPerfSection perf("Command/Copy");
__ASSERT__(!IsDir(path.c_str()));
log(format(" Copy '%1'").arg(path));
if (NeedsCompilation(hashes, {path}, {path}, {})) {
const auto src = FullInputPath(path), dst = FullOutputPath(path);
MkOutputTree(path);
CleanOutputs({path});
if (!CopyFile(src.c_str(), dst.c_str())) {
ReportFailedInput(src);
const json json_err = {{"type", "FailedToCopyInput"}, {"src", src}, {"dst", dst}};
log_error(json_err);
}
} else {
debug(" [O] Copy up to date");
}
}
//
static std::string profile = "default";
//
static void ProcessScene(const std::string &src, const std::string &dst) {
Scene scene;
PipelineResources res;
LoadSceneContext ctx;
bool load_res = false;
try {
load_res = LoadSceneFromFile(src.c_str(), scene, res, GetForwardPipelineInfo(), ctx, LSSF_All | LSSF_DoNotLoadResources);
} catch (...) {}
if (!load_res) {
ReportFailedInput(src);
const json json_err = {{"type", "FailedToLoadScene"}, {"src", src}};
log_error(json_err);
} else {
if (!SaveSceneBinaryToFile(dst.c_str(), scene, res)) {
const json json_err = {{"type", "FailedToSaveScene"}, {"dst", dst}};
log_error(json_err);
}
}
}
void Scene(std::map<std::string, Hash> &hashes, const std::string &path) {
ProfilerPerfSection perf("Command/Scene");
log(format(" Scene '%1'").arg(path));
Data build_ctx;
Write(build_ctx, std::string(get_version_string()));
Write(build_ctx, GetSceneBinaryFormatVersion());
if (NeedsCompilation(hashes, {path}, {path}, build_ctx)) {
const auto src = FullInputPath(path), dst = FullOutputPath(path);
MkOutputTree(path);
CleanOutputs({path});
task_queue.emplace_back(task{path, [=]() { return std::async(std::launch::async, ProcessScene, src, dst); }});
} else {
debug(" [O] Scene up to date");
}
}
//
void Copy(std::map<std::string, Hash> &hashes, const std::string &path);
//
static std::string Get_texconv_Format(const std::string &f) {
if (f == "BC1")
return "BC1_UNORM";
if (f == "BC2")
return "BC2_UNORM";
if (f == "BC3")
return "BC3_UNORM";
if (f == "BC4")
return "BC4_UNORM";
if (f == "BC5")
return "BC5_UNORM";
if (f == "BC6H")
return "BC6H_SF16";
if (f == "BC7")
return "BC7_UNORM";
return "";
}
static bool PreprocessTexture(const json &i_preprocess_texture, std::map<std::string, Hash> &hashes, std::string &path) {
// gather dependencies
std::set<std::string> dependencies;
for (const auto &i_directive : i_preprocess_texture.items())
if (i_directive.key() == "construct") {
if (i_directive.value().is_array()) {
const auto channel_count = i_directive.value().size();
if (channel_count < 3 || channel_count > 4) {
error(format("Cannot construct texture '%1' due to incorrect number of input channel (3 or 4 supported)").arg(path));
return false;
}
for (const auto &v : i_directive.value()) {
if (v.is_number_integer()) { // constant
;
} else if (v.is_string()) { // channel or texture path
const auto channel_v = v.get<std::string>();
if (channel_v == "R" || channel_v == "G" || channel_v == "B" || channel_v == "A")
dependencies.insert(path); // swizzle from input texture
else
dependencies.insert(channel_v); // swizzle from foreign texture
} else if (v.is_object()) { // channel or texture path
const auto dep_path = v["path"].get<std::string>();
const auto channel_v = v["channel"].get<std::string>();
dependencies.insert(dep_path);
}
}
} else {
error(format("Invalid texture construct tag for '%1', expected an array of channel source").arg(path));
return false;
}
}
const auto has_construct_directive = i_preprocess_texture.find("construct") != std::end(i_preprocess_texture);
if (!has_construct_directive)
dependencies.insert(path); // no construct directive implicitely means we need the input texture
// build context
Data build_ctx;
Write(build_ctx, i_preprocess_texture.dump()); // the whole preprocess block is our build context
// preprocess if input changed
const auto output_path = CutFileExtension(path) + ".tmp"; // preprocess into the input folder, output folder is *write-only*
if (!TestInputs(hashes, dependencies) && IsFile(FullInputPath(output_path).c_str()))
return true; // nothing to be done
// load all dependencies
std::map<std::string, Picture> picture_dependencies;
for (const auto &dep : dependencies)
if (!LoadPicture(picture_dependencies[dep], FullInputPath(dep).c_str())) {
error(format("Failed to load '%1' to construct '%2'").arg(dep).arg(path));
return false;
}
// determine output resolution
uint16_t out_width = 0, out_height = 0;
for (const auto &i : picture_dependencies) {
out_width = Max(out_width, i.second.GetWidth());
out_height = Max(out_height, i.second.GetHeight());
}
// conform dependencies
for (auto i : picture_dependencies)
if (i.second.GetWidth() != out_width && i.second.GetHeight() != out_height) {
// resize
picture_dependencies[i.first] = hg::Resize(i.second, out_width, out_height);
}
//
Picture out_pic;
if (!has_construct_directive)
out_pic = picture_dependencies[path]; // initialize with input if not constructed
for (const auto &i_directive : i_preprocess_texture.items())
if (i_directive.key() == "construct") {
out_pic = Picture(out_width, out_height, i_directive.value().size() == 3 ? PF_RGB24 : PF_RGBA32);
const auto out_stride = size_of(out_pic.GetFormat());
size_t channel_count = 0;
for (const auto &v : i_directive.value()) {
auto out = out_pic.GetData() + channel_count;
if (v.is_number_integer()) { // constant
const auto constant = v.get<int>();
for (uint16_t x = 0; x < out_width; ++x)
for (uint16_t y = 0; y < out_height; ++y) {
*out = constant;
out += out_stride;
}
} else if (v.is_string()) { // channel or texture path
const auto channel_v = v.get<std::string>();
uint8_t *in = nullptr;
size_t in_stride = 0;
if (channel_v == "R") {
auto &in_pic = picture_dependencies[path];
if (GetChannelCount(in_pic.GetFormat()) < 1) {
error(format("Cannot construct '%1' due to missing R channel").arg(path));
return false;
}
in = in_pic.GetData() + 0;
in_stride = size_of(in_pic.GetFormat());
} else if (channel_v == "G") {
auto &in_pic = picture_dependencies[path];
if (GetChannelCount(in_pic.GetFormat()) < 2) {
error(format("Cannot construct '%1' due to missing G channel").arg(path));
return false;
}
in = in_pic.GetData() + 1;
in_stride = size_of(in_pic.GetFormat());
} else if (channel_v == "B") {
auto &in_pic = picture_dependencies[path];
if (GetChannelCount(in_pic.GetFormat()) < 3) {
error(format("Cannot construct '%1' due to missing B channel").arg(path));
return false;
}
in = in_pic.GetData() + 2;
in_stride = size_of(in_pic.GetFormat());
} else if (channel_v == "A") {
auto &in_pic = picture_dependencies[path];
if (GetChannelCount(in_pic.GetFormat()) < 4) {
error(format("Cannot construct '%1' due to missing A channel").arg(path));
return false;
}
in = in_pic.GetData() + 3;
in_stride = size_of(in_pic.GetFormat());
} else {
auto &in_pic = picture_dependencies[channel_v];
if (GetChannelCount(in_pic.GetFormat()) <= channel_count) {
const char *channel_names[] = {"R", "G", "B", "A"};
error(format("Cannot construct '%1' due to '%2' missing %3 channel").arg(path).arg(channel_v).arg(channel_names[channel_count]));
return false;
}
in = in_pic.GetData() + channel_count;
in_stride = size_of(in_pic.GetFormat());
}
for (uint16_t y = 0; y < out_height; ++y)
for (uint16_t x = 0; x < out_width; ++x) {
*out = *in;
in += in_stride;
out += out_stride;
}
} else if (v.is_object()) { // channel and texture path
const auto dep_path = v["path"].get<std::string>();
auto &in_pic = picture_dependencies[dep_path];
const char *channel_names[] = {"R", "G", "B", "A"};
const auto input_channel_v = v["channel"].get<std::string>();
size_t input_channel = 5;
for (size_t c = 0; c < 4; ++c)
if (input_channel_v == channel_names[c]) {
input_channel = c;
break;
}
if (GetChannelCount(in_pic.GetFormat()) <= input_channel) {
error(format("Cannot construct '%1' due to '%2' missing %3 channel").arg(path).arg(dep_path).arg(input_channel));
return false;
}
uint8_t *in = in_pic.GetData() + input_channel;
size_t in_stride = size_of(in_pic.GetFormat());
for (uint16_t y = 0; y < out_height; ++y)
for (uint16_t x = 0; x < out_width; ++x) {
*out = *in;
in += in_stride;
out += out_stride;
}
}
++channel_count;
}
if (!SaveTGA(out_pic, FullInputPath(output_path).c_str())) {
error(format("Failed to save constructed picture as '%1'").arg(output_path));
return false;
}
}
path = output_path;
return true;
}
void Texture(std::map<std::string, Hash> &hashes, std::string path) {
ProfilerPerfSection perf("Command/Texture");
log(format(" Texture '%1'").arg(path));
std::string in_path = path; // may be changed by a construct directive
//
const auto meta_db = LoadMeta(path);
std::string type = "Standard";
GetMetaValue(meta_db, "type", type, profile);
int max_size = 16384;
GetMetaValue(meta_db, "max-size", max_size, profile);
std::string compression = "RAW";
GetMetaValue(meta_db, "compression", compression, profile);
bool generate_mips = true;
GetMetaValue(meta_db, "generate-mips", generate_mips, profile);
bool generate_probe = false;
GetMetaValue(meta_db, "generate-probe", generate_probe, profile);
int max_probe_size = 512;
GetMetaValue(meta_db, "max-probe-size", max_probe_size, profile);
bool radiance_edge_fixup = false;
GetMetaValue(meta_db, "radiance-edge-fixup", radiance_edge_fixup, profile);
// preprocessing
const json *i_preprocess;
if (GetMetaTag(meta_db, "preprocess", i_preprocess, profile)) {
if (PreprocessTexture(*i_preprocess, hashes, in_path)) {
;
} else {
const json json_err = {{"type", "FailedTexturePreprocessing"}};
log_error(json_err);
}
}
//
if (api == "DX12" || api == "DX11" || api == "GL" || api == "GLES" || api == "VK" || api == "MTL" ) {
const auto src = FullInputPath(in_path);
if (type == "Copy") {
Copy(hashes, in_path);
} else if (type != "Ignore") {
const auto dst = FullOutputPath(path);
Data build_ctx;
Write(build_ctx, max_size);
Write(build_ctx, generate_mips);
Write(build_ctx, compression);
const auto texconv_fmt = Get_texconv_Format(compression);
const auto use_texconv = !toolchain.texconv.empty() && !texconv_fmt.empty();
if (use_texconv)
Write(build_ctx, "texconv");
if (toolchain.texturec.empty()) {
warn(" Skipping, no compiler found for texture resource");
} else {
if (NeedsCompilation(hashes, {in_path}, {path}, build_ctx)) {
MkOutputTree(path);
CleanOutputs({path});
std::string cmd;
if (use_texconv) { // favor the much faster texconv over texturec
cmd = format("%1 -y -nologo -maxsize %2 -f %3 -bc x").arg(toolchain.texconv).arg(max_size).arg(texconv_fmt).str();
if (!generate_mips)
cmd += " -m 1";
cmd += format(" \"%1\" \"%2\"").arg(src).arg(dst).str();
}
if (cmd.empty()) { // fallback to texturec
cmd = format("%1 -f \"%2\" -o \"%3\" --as dds --max %4").arg(toolchain.texturec).arg(src).arg(dst).arg(max_size).str();
if (compression != "RAW")
cmd += format(" -t %1").arg(compression).str();
else if (assetc::api == "DX11")
cmd += " -t BGRA8"; // prevent useless swizzle at runtime
else
cmd += " -t RGBA8"; // prevent useless swizzle at runtime
if (generate_mips)
cmd += " -m";
}
PushAsyncProcessTask(path + " (Texture)", cmd, cwd);
} else {
debug(" [O] Texture up to date");
}
}
}
if (generate_probe) {
const auto dst = FullOutputPath(path + ".radiance");
Data build_ctx;
Write(build_ctx, max_probe_size);
Write(build_ctx, radiance_edge_fixup);
Write(build_ctx, 20); // gloss scale
Write(build_ctx, 0); // gloss bias
if (toolchain.cmft.empty()) {
warn(" Skipping, no compiler found for radiance probe resource");
} else {
if (NeedsCompilation(hashes, {in_path}, {path + ".radiance"}, build_ctx)) {
MkOutputTree(path + ".radiance");
CleanOutputs({path + ".radiance"});
const auto cmd = format("%1 --input \"%2\" --output0 \"%3\" --output0params dds,rgba16f,cubemap --useOpenCL true --filter radiance "
"--srcFaceSize %4 --edgeFixup %5 --glossScale 20 --glossBias 0")
.arg(toolchain.cmft)
.arg(src)
.arg(dst)
.arg(max_probe_size)
.arg(radiance_edge_fixup ? "warp" : "none")
.str();
PushAsyncProcessTask(path + " (Radiance Probe)", cmd, cwd);
} else {
debug(" [O] Texture radiance up to date");
}
}
}
if (generate_probe) {
const auto dst = FullOutputPath(path + ".irradiance");
Data build_ctx;
Write(build_ctx, max_probe_size);
if (toolchain.cmft.empty()) {
warn(" Skipping, no compiler found for irradiance probe resource");
} else {
if (NeedsCompilation(hashes, {in_path}, {path + ".irradiance"}, build_ctx)) {
MkOutputTree(path + ".irradiance");
CleanOutputs({path + ".irradiance"});
const auto cmd =
format("%1 --input \"%2\" --output0 \"%3\" --output0params dds,rgba16f,cubemap --useOpenCL false --filter irradiance --srcFaceSize %4")
.arg(toolchain.cmft)
.arg(src)
.arg(dst)
.arg(max_probe_size)
.str();
PushAsyncProcessTask(path + " (Irradiance Probe)", cmd, cwd);
} else {
debug(" [O] Texture irradiance up to date");
}
}
}
} else {
const json json_err = {{"type", "UnsupportedTextureAPI"}, {"api", api}};
log_error(json_err);
}
}
//
static void ProcessGeometry(const std::string &src, const std::string &dst, ModelOptimisationLevel optimisation_level) {
const auto geo = LoadGeometryFromFile(src.c_str());
if (!Validate(geo)) {
const json json_err = {{"type", "InvalidGeometry"}, {"dst", dst}};
log_error(json_err);
} else {
if (!SaveGeometryModelToFile(dst.c_str(), geo, optimisation_level)) {
const json json_err = {{"type", "FailedToSaveModel"}, {"dst", dst}};
log_error(json_err);
}
}
}
void Geometry(std::map<std::string, Hash> &hashes, const std::string &path) {
ProfilerPerfSection perf("Command/Geometry");
log(format(" Geometry '%1'").arg(path));
const auto meta_db = LoadMeta(path);
bool cook_model = true;
GetMetaValue(meta_db, "cook-model", cook_model, profile);
ModelOptimisationLevel optimisation_level = MOL_Full;
Data build_ctx;
Write(build_ctx, std::string(get_version_string()));
Write(build_ctx, GetModelBinaryFormatVersion());
Write(build_ctx, cook_model);
Write(build_ctx, optimisation_level);
if (NeedsCompilation(hashes, {path}, {path}, build_ctx)) {
const auto src = FullInputPath(path), dst = FullOutputPath(path);
MkOutputTree(path);
CleanOutputs({path});
task_queue.emplace_back(task{path, [=]() { return std::async(std::launch::async, ProcessGeometry, src, dst, optimisation_level); }});
} else {
debug(" [O] Geometry up to date");
}
}
//
void GatherShaderDependencies(const std::string &path, std::set<std::string> &deps, const std::string &base) {
const auto rel_path = base + path;
if (deps.find(rel_path) != std::end(deps))
return; // processed already, don't need no cyclic references
deps.insert(rel_path);
const auto src = hg::FileToString(FullInputPath(rel_path).c_str());
static std::regex include_regex("#include\\s*(?:\"|<)(.+)(?:\"|>)");
const auto base_path = hg::GetFilePath(rel_path); // #include directives are relative to the current source
for (std::sregex_iterator i = std::sregex_iterator(src.begin(), src.end(), include_regex); i != std::sregex_iterator(); ++i)
GatherShaderDependencies((*i)[1], deps, base_path); // recurse down the dependency tree
}
void GatherShaderDependencies(const std::set<std::string> &paths, std::set<std::string> &deps) {
for (const auto &path : paths)
GatherShaderDependencies(path, deps, "");
}
//
static void BuildComputeShader(std::map<std::string, Hash> &hashes, const std::string &cs_path, const std::string &defines) {
ProfilerPerfSection perf("Command/ComputeShader");
std::string cs_profile;
if (api == "DX11" || api == "DX12") {
cs_profile = "cs_5_0";
} else if (api == "GL") {
cs_profile = "130";
} else if (api == "GLES") {
// no profile => essl
} else if (api == "VK") {
cs_profile = "spirv";
} else if (api == "MTL") {
cs_profile = "metal";
} else {
const json json_err = {{"type", "UnsupportedComputeAPI"}, {"api", api}};
log_error(json_err);
return;
}
const std::string optim_flags = compile_with_debug_info ? "-O 0 --debug" : "-O 3";
//
const auto vs_defines = std::string("IS_COMPUTE_SHADER=1;") + defines;
Data cs_build_ctx;
Write(cs_build_ctx, cs_profile);
Write(cs_build_ctx, optim_flags);
Write(cs_build_ctx, vs_defines);
if (toolchain.shaderc.empty()) {
warn(" Skipping, no compiler found for compute resource");
} else {
std::set<std::string> inputs;
GatherShaderDependencies({cs_path}, inputs);
if (NeedsCompilation(hashes, inputs, {cs_path}, cs_build_ctx)) {
if (!cs_profile.empty()) // GLES profile must be empty...
cs_profile = "-p " + cs_profile;
const auto cs_out = FullOutputPath(cs_path);
MkOutputTree(cs_path);
CleanOutputs({cs_path});
const auto cs_cmd = format("%1 -f \"%2\" -o \"%3\" --platform %4 %5 %6 --type compute --define \"%7\"")
.arg(toolchain.shaderc)
.arg(FullInputPath(cs_path))
.arg(cs_out)
.arg(assetc::platform)
.arg(cs_profile)
.arg(optim_flags)
.arg(vs_defines);
PushAsyncProcessTask(cs_path + " (Compute Shader)", cs_cmd, cwd);
} else {
debug(" [O] Compute shader up to date");
}
}
}
static void ComputeShader(std::map<std::string, Hash> &hashes, const std::string &cs_path) {
log(format(" Compute Shader '%1'").arg(cs_path));
BuildComputeShader(hashes, cs_path, global_shader_defines);
}
//
static void BuildShader(std::map<std::string, Hash> &hashes, const std::string &name, const std::string &vs_path, const std::string &fs_path,
const std::string &varying_path, const std::string &defines) {
ProfilerPerfSection perf("Command/Shader");
std::string vs_profile, fs_profile;
if (api == "DX11" || api == "DX12") {
vs_profile = "vs_5_0";
fs_profile = "ps_5_0";
} else if (api == "GL") {
vs_profile = fs_profile = "130";
} else if (api == "GLES") {
// no profile => essl
} else if (api == "VK") {
vs_profile = fs_profile = "spirv";
} else if (api == "MTL") {
vs_profile = fs_profile = "metal";
} else {
const json json_err = {{"type", "UnsupportedShaderAPI"}, {"api", api}};
log_error(json_err);
return;
}
const std::string optim_flags = compile_with_debug_info ? "--debug" : "-O 3";
//
const auto vs_defines = std::string("IS_VERTEX_SHADER=1;") + defines;
Data vs_build_ctx;
Write(vs_build_ctx, vs_profile);
Write(vs_build_ctx, optim_flags);
Write(vs_build_ctx, vs_defines);
const auto vs_name = format("%1.vsb").arg(name);
if (toolchain.shaderc.empty()) {
warn(" Skipping, no compiler found for shader resource");
} else {
std::set<std::string> inputs;
GatherShaderDependencies({vs_path, varying_path}, inputs);
if (NeedsCompilation(hashes, inputs, {vs_name}, vs_build_ctx)) {
if (!vs_profile.empty()) // GLES profile must be empty...
vs_profile = "-p " + vs_profile;
const auto vs_out = FullOutputPath(vs_name);
MkOutputTree(vs_name);
CleanOutputs({vs_name});
const auto vs_cmd = format("%1 -f \"%2\" -o \"%3\" --varyingdef \"%4\" --type v --platform %5 %6 %7 --define \"%8\"")
.arg(toolchain.shaderc)
.arg(FullInputPath(vs_path))
.arg(vs_out)
.arg(FullInputPath(varying_path))
.arg(assetc::platform)
.arg(vs_profile)
.arg(optim_flags)
.arg(vs_defines);
PushAsyncProcessTask(name + " (Vertex Shader)", vs_cmd, cwd);
} else {
debug(" [O] Vertex shader up to date");
}
}
//
const auto fs_defines = std::string("IS_FRAGMENT_SHADER=1;") + defines;
Data fs_build_ctx;
Write(fs_build_ctx, fs_profile);
Write(fs_build_ctx, optim_flags);
Write(fs_build_ctx, fs_defines);
const auto fs_name = format("%1.fsb").arg(name);
if (toolchain.shaderc.empty()) {
warn(" Skipping, no compiler found for shader resource");
} else {
std::set<std::string> inputs;
GatherShaderDependencies({fs_path, varying_path}, inputs);
if (NeedsCompilation(hashes, inputs, {fs_name}, fs_build_ctx)) {
if (!fs_profile.empty())
fs_profile = "-p " + fs_profile;
const auto fs_out = FullOutputPath(fs_name);
MkOutputTree(fs_name);
CleanOutputs({fs_name});
const auto fs_cmd = format("%1 -f \"%2\" -o \"%3\" --varyingdef \"%4\" --type f --platform %5 %6 %7 --define \"%8\"")
.arg(toolchain.shaderc)
.arg(FullInputPath(fs_path))
.arg(fs_out)
.arg(FullInputPath(varying_path))
.arg(assetc::platform)
.arg(fs_profile)
.arg(optim_flags)
.arg(fs_defines);
PushAsyncProcessTask(name + " (Fragment Shader)", fs_cmd, cwd);
} else {
debug(" [O] Pixel shader up to date");
}
}
}
static void Shader(std::map<std::string, Hash> &hashes, const std::string &vs_path, const std::string &fs_path, const std::string &varying_path) {
const auto name = slice(vs_path, 0, -6);
log(format(" Shader '%1'").arg(name));
BuildShader(hashes, name, vs_path, fs_path, varying_path, global_shader_defines);
}
//
static void BuildPipelineShaderVariant(std::map<std::string, Hash> &hashes, const std::string &name, const std::string &feats_path, const std::string &vs_path,
const std::string &fs_path, const std::string &varying_path, const std::string &defines) {
ProfilerPerfSection perf("Manage/BuildPipelineShaderVariant");
size_t config = 0;
for (auto &variant : pipeline.configs) {
log(format(" Pipeline shader variant '%1' for pipeline config %2").arg(name).arg(config));
const auto variant_name = format("%1_pipe-%2-cfg-%3").arg(name).arg(pipeline.name).arg(config++);
const auto variant_defines = join(std::begin(variant), std::end(variant), ";") + ";" + defines;
BuildShader(hashes, variant_name, vs_path, fs_path, varying_path, global_shader_defines + variant_defines);
}
}
//
static bool IterateProgramFeatureStates(const std::vector<PipelineProgramFeature> &feats, std::vector<int> &states,
const std::function<void(const std::vector<PipelineProgramFeature> &feats, const std::vector<int> &states)> &process_states, int i = 0) {
if (states.empty()) {
process_states(feats, states); // single program for no feature pipeline program
return false;
}
if (i == states.size())
return false;
for (int j = 0; j < GetPipelineProgramFeatureStateCount(feats[i]); ++j) {
states[i] = j;
if (!IterateProgramFeatureStates(feats, states, process_states, i + 1))
process_states(feats, states);
}
return true;
}
static std::string GetDefines(const std::vector<PipelineProgramFeature> &feats, const std::vector<int> &states) {
std::vector<std::string> defines;
bool has_ambient_uv_feature = false;
for (size_t i = 0; i < states.size(); ++i) {
const auto state = states[i];
if (feats[i] == OptionalBaseColorOpacityMap) {
defines.push_back(format("USE_BASE_COLOR_OPACITY_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalOcclusionRoughnessMetalnessMap) {
defines.push_back(format("USE_OCCLUSION_ROUGHNESS_METALNESS_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalDiffuseMap) {
defines.push_back(format("USE_DIFFUSE_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalSpecularMap) {
defines.push_back(format("USE_SPECULAR_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalLightMap) {
defines.push_back(format("USE_LIGHT_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalSelfMap) {
defines.push_back(format("USE_SELF_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalOpacityMap) {
defines.push_back(format("USE_OPACITY_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalAmbientMap) {
defines.push_back(format("USE_AMBIENT_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalReflectionMap) {
defines.push_back(format("USE_REFLECTION_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalNormalMap) {
defines.push_back(format("USE_NORMAL_MAP=%1").arg(state ? "1" : "0"));
} else if (feats[i] == NormalMapInWorldSpace) {
defines.push_back(format("NORMAL_MAP_IN_WORLD_SPACE=%1").arg(state ? "1" : "0"));
} else if (feats[i] == DiffuseUV1) {
defines.push_back(format("DIFFUSE_UV_CHANNEL=%1").arg(state));
} else if (feats[i] == SpecularUV1) {
defines.push_back(format("SPECULAR_UV_CHANNEL=%1").arg(state));
} else if (feats[i] == AmbientUV1) {
defines.push_back(format("AMBIENT_UV_CHANNEL=%1").arg(state));
has_ambient_uv_feature = true;
} else if (feats[i] == OptionalSkinning) {
defines.push_back(format("ENABLE_SKINNING=%1").arg(state ? "1" : "0"));
} else if (feats[i] == OptionalAlphaCut) {
defines.push_back(format("ENABLE_ALPHA_CUT=%1").arg(state ? "1" : "0"));
}
}
if (!has_ambient_uv_feature)
defines.push_back("AMBIENT_UV_CHANNEL=1"); // [EJ02042022] default to legacy behavior
return join(std::begin(defines), std::end(defines), ";");
}
//
static void PipelineShader(
std::map<std::string, Hash> &hashes, const std::string &hps_path, const std::string &vs_path, const std::string &fs_path, const std::string &varying_path) {
ProfilerPerfSection perf("Command/PipelineShader");
log(format(" Pipeline shader '%1' for pipeline '%2'").arg(hps_path).arg(pipeline.name));
Copy(hashes, hps_path);
const auto name = CutFileExtension(hps_path);
bool success;
const auto feats = LoadPipelineProgramFeaturesFromFile(PathJoin({input_dir, hps_path}).c_str(), success);
if (!success) {
const json json_err = {{"type", "FailedToLoadPipelineProgramFeatures"}, {"path", hps_path}};
log_error(json_err);
}
std::vector<int> states(feats.size(), 0);
IterateProgramFeatureStates(feats, states, [&](const std::vector<PipelineProgramFeature> &feats, const std::vector<int> &states) {
const auto variant_defines = GetDefines(feats, states);
const auto variant_name = GetPipelineProgramVariantName(name.c_str(), feats, states);
BuildPipelineShaderVariant(hashes, variant_name, hps_path, vs_path, fs_path, varying_path, variant_defines);
});
}
//
static void LuaScript(std::map<std::string, Hash> &hashes, const std::string &path) {
ProfilerPerfSection perf("Command/LuaScript");
log(format(" Lua script '%1'").arg(path));
if (toolchain.luac.empty()) {
warn(" Skipping, no compiler found for Lua script resource");
} else {
if (NeedsCompilation(hashes, {path}, {path}, {})) {
const auto src = FullInputPath(path), dst = FullOutputPath(path);
MkOutputTree(path);
CleanOutputs({path});
const auto cmd = format("%1 -o \"%3\" -s \"%2\"").arg(toolchain.luac).arg(src).arg(dst);
PushAsyncProcessTask(path, cmd, cwd);
} else {
debug(" [O] Lua script up to date");
}
}
}
//
static void Physics(std::map<std::string, Hash> &hashes, const std::string &path) {
ProfilerPerfSection perf("Command/Physics");
log(format(" Physics resource '%1'").arg(path));
if (toolchain.bulletc.empty()) {
debug(" Skipping, no compiler found for physics bullet resource");
} else {
if (NeedsCompilation(hashes, {path}, {path}, {})) {
const auto cwd = GetCurrentWorkingDirectory();
const auto src = FullInputPath(path), dst = FullOutputPath(path);
if (!CopyFile(src.c_str(), dst.c_str())) {
ReportFailedInput(src);
const json json_err = {{"type", "FailedToCopyInput"}, {"src", src}, {"dst", dst}};
log_error(json_err);
}
const auto cmd = format("\"%1\" \"%2\" \"%3_bullet\" -root \"%4\"").arg(toolchain.bulletc).arg(src).arg(dst).arg(input_dir);
PushAsyncProcessTask(path, cmd, cwd);
} else {
debug(" [O] Physics resource up to date");
}
}
}
static void PathFinding(std::map<std::string, Hash> &hashes, const std::string &path) {
ProfilerPerfSection perf("Command/PathFinding");
log(format(" Pathfinding resource '%1'").arg(path));
if (toolchain.recastc.empty()) {
debug(" Skipping, no compiler found for pathfinding resource");
} else {
if (NeedsCompilation(hashes, {path}, {path}, {})) {
const auto src = FullInputPath(path), dst = FullOutputPath(path);
MkOutputTree(path);
CleanOutputs({path});
const auto cmd = format("%1 \"%2\" \"%3\" -root \"%4\"").arg(toolchain.recastc).arg(src).arg(dst).arg(input_dir);
PushAsyncProcessTask(path, cmd, GetCurrentWorkingDirectory());
} else {
debug(" [O] Pathfinding resource up to date");
}
}
}
//
enum class AssetType { Unprocessed, Ignore, Broken, Scene, Texture, Geometry, Lua, Shader, PipelineShader, Physics, PathFinding, ComputeShader, Count };
#if 0
// CWE 561: The function 'AssetTypeToString' is never used.
static const std::string& AssetTypeToString(AssetType type) {
static const std::string types[static_cast<int>(AssetType::Count) + 1] = {
"Unprocessed", "Ignore", "Broken", "Scene", "Texture", "Geometry", "Lua", "Shader", "PipelineShader", "Physics", "PathFinding", "ComputeShader", "Undefined"};
return types[int(type)];
}
#endif
struct AssetConfig {
AssetType type;
};
static AssetType GetAssetFileType(std::string path, const std::vector<std::string> &all_files, std::vector<std::string> &out_files) {
static const std::set<std::string> texture_exts = {"bmp", "exr", "gif", "jpg", "jpeg", "hdr", "png", "psd", "tga"};
static const std::set<std::string> ignored_exts = {"tmp"};
const auto ext = tolower(GetFileExtension(path));
if (ext == "scn") {
out_files.push_back(path);
return AssetType::Scene;
} else if (texture_exts.find(ext) != std::end(texture_exts)) {
out_files.push_back(path);
return AssetType::Texture;
} else if (ignored_exts.find(ext) != std::end(ignored_exts)) {
return AssetType::Ignore;
} else if (ext == "geo") {
out_files.push_back(path);
return AssetType::Geometry;
} else if (ext == "lua") {
out_files.push_back(path);
return AssetType::Lua;
} else if (ext == "sc") {
std::string name;
if (ends_with(path, "_vs.sc") || ends_with(path, "_fs.sc"))
name = slice(path, 0, -6);
else if (ends_with(path, "_varying.def"))
name = slice(path, 0, -11);
else
name = slice(path, 0, -3);
const auto hps_name = name + ".hps", vs_name = name + "_vs.sc", fs_name = name + "_fs.sc", varying_name = name + "_varying.def";
const auto hps_i = std::find(std::begin(all_files), std::end(all_files), hps_name);
const auto vs_i = std::find(std::begin(all_files), std::end(all_files), vs_name);
const auto fs_i = std::find(std::begin(all_files), std::end(all_files), fs_name);
const auto varying_i = std::find(std::begin(all_files), std::end(all_files), varying_name);
const bool has_hps = hps_i != std::end(all_files), has_vs = vs_i != std::end(all_files), has_fs = fs_i != std::end(all_files),
has_varying = varying_i != std::end(all_files);
if (has_hps) { // that's a pipeline shader
if (has_vs && has_fs && has_varying) {
out_files.push_back(hps_name);
out_files.push_back(vs_name);
out_files.push_back(fs_name);
out_files.push_back(varying_name);
return AssetType::PipelineShader;
}
out_files.push_back(hps_name);
} else {
if (has_vs && has_fs && has_varying) {
out_files.push_back(vs_name);
out_files.push_back(fs_name);
out_files.push_back(varying_name);
return AssetType::Shader;
} else {
out_files.push_back(path);
return AssetType::ComputeShader;
}
}
if (has_vs)
out_files.push_back(vs_name);
if (has_fs)
out_files.push_back(fs_name);
if (has_varying)
out_files.push_back(varying_name);
return AssetType::Broken;
} else if (ext == "hps") {
const auto name = slice(path, 0, -4);
const auto vs_name = name + "_vs.sc", fs_name = name + "_fs.sc", varying_name = name + "_varying.def";
const auto vs_i = std::find(std::begin(all_files), std::end(all_files), vs_name);
const auto fs_i = std::find(std::begin(all_files), std::end(all_files), fs_name);
const auto varying_i = std::find(std::begin(all_files), std::end(all_files), varying_name);
const bool has_vs = vs_i != std::end(all_files), has_fs = fs_i != std::end(all_files), has_varying = varying_i != std::end(all_files);
out_files.push_back(path);
if (has_vs)
out_files.push_back(vs_name);
if (has_fs)
out_files.push_back(fs_name);
if (has_varying)
out_files.push_back(varying_name);
if (has_vs && has_fs && has_varying)
return AssetType::PipelineShader;
return AssetType::Broken;
} else if (ext == "physics") {
out_files.push_back(path);
return AssetType::Physics;
} else if (ext == "pathfinding") {
out_files.push_back(path);
return AssetType::PathFinding;
}
out_files.push_back(path);
return AssetType::Unprocessed;
}
//
static std::vector<std::string> DirEntriesToPaths(const std::vector<DirEntry> &entries) {
std::vector<std::string> paths;
std::transform(std::begin(entries), std::end(entries), std::back_inserter(paths), [](const DirEntry &e) { return e.name; });
return paths;
}
static std::vector<std::string> blacklist = {"_meta", "_prod", ".git", ".svn"};
static std::map<std::vector<std::string>, AssetConfig> ClassifyInputs(
const std::vector<std::string> &all_files, const std::vector<std::string> &files_to_classify) {
ProfilerPerfSection perf("Manage/ClassifyInputs");
std::map<std::vector<std::string>, AssetConfig> asset_configs;
size_t i = 0;
const size_t file_count = files_to_classify.size();
for (auto &path : files_to_classify) {
if (progress)
std::cout << "-> Progress: " << i * classify_progress_weight / file_count << "% (" << path << ")" << std::endl;
++i;
bool is_blacklisted = false;
for (auto &prefix : blacklist)
if (starts_with(path, prefix)) {
is_blacklisted = true;
break;
}
if (is_blacklisted)
continue;
bool already_classified = false;
for (auto &cfg : asset_configs)
if (std::find(std::begin(cfg.first), std::end(cfg.first), path) != std::end(cfg.first)) {
already_classified = true;
break;
}
if (already_classified)
continue; // file already classified
std::vector<std::string> out_files;
const auto type = GetAssetFileType(path, all_files, out_files);
asset_configs[std::move(out_files)] = {type};
}
return asset_configs;
}
static std::vector<std::string> GetInputDirFiles() { return DirEntriesToPaths(ListDirRecursive(input_dir.c_str(), DE_File)); }
static std::map<std::vector<std::string>, AssetConfig> ClassifyInputDir() {
const auto all_files = GetInputDirFiles();
return ClassifyInputs(all_files, all_files);
}
//
static bool CompileClassifiedInputs(const std::map<std::vector<std::string>, AssetConfig> &inputs) {
ProfilerPerfSection perf("Manage/CompileClassifiedInputs");
const auto t_start = time_now();
std::cout << "Compilation start" << std::endl;
std::map<std::string, Hash> updated_hashes;
const auto input_count = inputs.size();
size_t j = 0;
for (auto &i : inputs) {
const auto &names = i.first;
if (!names.empty()) {
const auto &config = i.second;
if (progress)
std::cout << "-> Progress: " << j * compile_progress_weight / input_count + classify_progress_weight << "% (" << names[0] << ")" << std::endl;
++j;
if (config.type == AssetType::Unprocessed)
Copy(updated_hashes, names[0]);
else if (config.type == AssetType::Scene)
Scene(updated_hashes, names[0]);
else if (config.type == AssetType::Texture)
Texture(updated_hashes, names[0]);
else if (config.type == AssetType::Geometry)
Geometry(updated_hashes, names[0]);
else if (config.type == AssetType::Lua)
LuaScript(updated_hashes, names[0]);
else if (config.type == AssetType::ComputeShader)
ComputeShader(updated_hashes, names[0]);
else if (config.type == AssetType::Shader)
Shader(updated_hashes, names[0], names[1], names[2]);
else if (config.type == AssetType::PipelineShader)
PipelineShader(updated_hashes, names[0], names[1], names[2], names[3]);
else if (config.type == AssetType::Physics)
Physics(updated_hashes, names[0]);
else if (config.type == AssetType::PathFinding)
PathFinding(updated_hashes, names[0]);
else if (config.type == AssetType::Broken)
((void)0);
}
if (!IsPollProcessUp()) {
std::cout << "Compilation aborted after " << time_to_ms(time_now() - t_start) << "ms, poll process down" << std::endl;
return false;
}
}
for (const auto &h : updated_hashes)
compilation_db.source_hashes[h.first] = h.second; // commit updated hashes
assetc::RunTaskQueue();
assetc::SaveCompilationDB();
std::cout << "Compilation done, took " << time_to_ms(time_now() - t_start) << "ms" << std::endl;
return true;
}
//
static bool clean_outputs_for_removed_inputs = true;
static void CleanOutputsForRemovedInputs() {
ProfilerPerfSection perf("Manage/CleanOutputsForRemovedInputs");
std::map<std::string, std::set<std::string>> db_input_to_outputs;
for (const auto &i : compilation_db.output_to_inputs)
for (const auto &input : i.second)
db_input_to_outputs[input].insert(i.first);
const auto input_files = GetInputDirFiles(); // available inputs
size_t removed = 0;
for (const auto &i : db_input_to_outputs)
if (std::find(std::begin(input_files), std::end(input_files), i.first) == std::end(input_files)) {
for (const auto &output : i.second) // DB input is missing from input files, remove its associated outputs
if (Unlink(FullOutputPath(output).c_str()))
++removed;
}
std::cout << "Removed " << removed << " outputs due to missing input" << std::endl;
}
//
static void DaemonMode() {
log("Entering daemon mode, press Ctrl+C to close\n");
WatchDirectory(input_dir, true);
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
const auto events = GetDirectoryWatchEvents(input_dir);
std::set<std::string> modified_files;
for (const auto &ev : events)
if ((ev.type == WatchEvent::FileAdded) || (ev.type == WatchEvent::FileModified))
if (IsFile(PathJoin({input_dir, ev.path}).c_str()))
modified_files.insert(ev.path);
// change in .meta must trickle down to the file it refers to
std::set<std::string> modified_files_;
for (const auto &i : modified_files) {
modified_files_.insert(i);
if (ends_with(i, ".meta", insensitive))
modified_files_.insert(left(i, -5));
}
if (!modified_files_.empty()) {
log(format("File system changes detected (%1):").arg(modified_files_.size()));
for (const auto &f : modified_files_)
log((std::string(" - ") + f).c_str());
if (assetc::clean_outputs_for_removed_inputs)
assetc::CleanOutputsForRemovedInputs();
if (!CompileClassifiedInputs(ClassifyInputs(GetInputDirFiles(), {std::begin(modified_files_), std::end(modified_files_)})))
break;
log("Press Ctrl+C to close\n");
}
}
UnwatchDirectory(input_dir);
}
//
static void OutputPerfReport() {
const auto profile = EndProfilerFrame();
time_ns total = 0;
for (auto &task : profile.tasks)
if (task.name == "Total")
total = task.duration;
if (total) {
std::cout << std::endl << "Performance report" << std::endl;
for (auto &task : profile.tasks)
std::cout << " - " << task.name << ": "
<< "(" << (task.duration * 100 / total) << "%) " << FormatTime(task.duration) << " (" << task.section_indexes.size() << " call)"
<< std::endl;
std::cout << std::endl;
}
}
} // namespace assetc
//
static void OutputUsage(const CmdLineFormat &cmd_format) {
std::cout << "Usage: assetc " << word_wrap(FormatCmdLineArgs(cmd_format), 120, 14) << std::endl << std::endl;
std::cout << FormatCmdLineArgsDescription(cmd_format);
}
#if WIN32
int wmain(int narg, wchar_t **argv) {
std::vector<std::string> args_utf8(narg + 1);
for (int i = 0; i < narg; ++i)
args_utf8[i] = hg::wchar_to_utf8(argv[i]);
std::vector<const char *> _args(narg + 1);
for (int i = 0; i < narg; ++i)
_args[i] = args_utf8[i].c_str();
const char **args = _args.data();
#else
int main(int narg, const char **args) {
#endif
const auto exe_path = GetFilePath(args[0]);
std::cout << "Harfang ASSETC 1.1" << std::endl;
assetc::cwd = GetCurrentWorkingDirectory();
const auto default_toolchain_path = format("toolchains/host-%1-target-%2").arg(get_host_string()).arg(get_target_string()).str();
// parse command line
CmdLineFormat cmd_format = {
{
{"-daemon", "Run in the background and watch for modifications to the input folder"},
{"-progress", "Output progress to the standard C output stream"},
{"-log_errors_to_stderr", "Log errors as JSON to the standard C error output stream"},
{"-debug", "Compile in debug mode (eg. output debug informations in shader)"},
{"-quiet", "Disable all build information but errors"},
{"-verbose", "Output additional information about the compilation process"},
{"-fast_check", "Perform modification detection using input file timestamp"},
{"-no_clean_removed_inputs", "Do not remove outputs for removed input files"},
},
{
{"-job", "Maximum number of parallel job (0 - automatic)", true},
{"-toolchain", format("Path to the toolchain folder (default: %1)").arg(default_toolchain_path), true},
{"-platform", "Select the target platform to compile for (defaults to current platform)", true},
{"-api", "Select the platform graphic API to compile for", true},
{"-defines", "Semicolon separated defines to pass to shaderc (eg. FLAG;VALUE=2)", true},
{"-poll_pid", "Poll the provided process and exit assetc if down", true},
},
{
{"input", "Input folder to compile sources from"},
{"output", "Output folder for compiled assets", true},
},
{
{"-d", "-daemon"},
{"-l", "-log_errors_to_stderr"},
{"-j", "-job"},
{"-t", "-toolchain"},
{"-p", "-platform"},
{"-q", "-quiet"},
{"-v", "-verbose"},
{"-D", "-defines"},
{"-f", "-fast_check"},
{"-n", "-no_clean_removed_inputs"},
},
};
CmdLineContent cmd_content;
if (!ParseCmdLine({args + 1, args + narg}, cmd_format, cmd_content)) {
OutputUsage(cmd_format);
return -1;
}
// toolchain
const auto toolchain_path = GetCmdLineSingleValue(cmd_content, "-toolchain", PathJoin({exe_path, default_toolchain_path}));
assetc::SetToolchain(toolchain_path);
assetc::ToolchainExists();
/*
if (!assetc::ToolchainExists()) {
std::cout << "Error: Toolchain not found " << toolchain_path << std::endl;
OutputUsage(cmd_format);
return -4;
}
*/
assetc::api = GetCmdLineSingleValue(cmd_content, "-api", assetc::api);
assetc::platform = GetCmdLineSingleValue(cmd_content, "-platform", assetc::platform);
assetc::global_shader_defines = GetCmdLineSingleValue(cmd_content, "-defines", "");
if (!assetc::global_shader_defines.empty() && !ends_with(assetc::global_shader_defines, ";"))
assetc::global_shader_defines += ";";
assetc::max_async_jobs = GetCmdLineSingleValue(cmd_content, "-job", 0);
if (assetc::max_async_jobs < 1)
assetc::max_async_jobs = std::thread::hardware_concurrency();
assetc::progress = GetCmdLineFlagValue(cmd_content, "-progress");
assetc::log_errors_to_stderr = GetCmdLineFlagValue(cmd_content, "-log_errors_to_stderr");
assetc::compile_with_debug_info = GetCmdLineFlagValue(cmd_content, "-debug");
assetc::fast_check = GetCmdLineFlagValue(cmd_content, "-fast_check");
assetc::poll_process_id = GetCmdLineSingleValue(cmd_content, "-poll_pid", 0);
assetc::clean_outputs_for_removed_inputs = !GetCmdLineFlagValue(cmd_content, "-no_clean_removed_inputs");
assetc::SetRenderPipeline(GetForwardPipelineInfo());
//
int log_level = LL_Normal | LL_Warning | LL_Error;
if (GetCmdLineFlagValue(cmd_content, "-quiet"))
log_level = LL_Warning | LL_Error;
if (GetCmdLineFlagValue(cmd_content, "-verbose"))
log_level = LL_All; // verbose logs all
set_log_level(log_level);
// input folder compilation mode
if (cmd_content.positionals.empty()) {
std::cout << "Error: No input folder specified" << std::endl;
OutputUsage(cmd_format);
return -2;
}
assetc::SetInputDir(cmd_content.positionals[0]);
if (!IsDir(assetc::input_dir.c_str())) {
std::cout << "Error: Input dir '" << assetc::input_dir << "' not found" << std::endl;
OutputUsage(cmd_format);
return -3;
}
if (cmd_content.positionals.size() > 1)
assetc::SetOutputDir(cmd_content.positionals[1]);
else
assetc::SetOutputDir(assetc::input_dir + "_compiled");
//
auto exe_path_or_nothing = [](const std::string &path) { return path.empty() ? "-" : path; };
log(format("> Input dir: %1").arg(assetc::input_dir));
log(format("> Output dir: %1").arg(assetc::output_dir));
log("");
log(format("> Target platform: %1").arg(assetc::platform));
log(format("> Target graphics API: %1").arg(assetc::api));
log(format("> Target pipeline: %1").arg(assetc::pipeline.name));
log("");
log(format("> Using %1 parallel job").arg(assetc::max_async_jobs));
log(format("> Toolchain compilers (%1):").arg(toolchain_path));
log(format(" - Shader %1").arg(exe_path_or_nothing(assetc::toolchain.shaderc)));
log(format(" - Texture %1").arg(exe_path_or_nothing(assetc::toolchain.texturec)));
log(format(" - Probe %1").arg(exe_path_or_nothing(assetc::toolchain.cmft)));
log(format(" - Lua %1").arg(exe_path_or_nothing(assetc::toolchain.luac)));
log(format(" - Pathfinding %1").arg(exe_path_or_nothing(assetc::toolchain.recastc)));
log("");
// initial run over input directory (process all files)
{
ProfilerPerfSection perf("Total");
assetc::LoadCompilationDB();
if (assetc::clean_outputs_for_removed_inputs)
assetc::CleanOutputsForRemovedInputs();
if (assetc::CompileClassifiedInputs(assetc::ClassifyInputDir())) {
// enter daemon mode, process input dir files as they change
if (GetCmdLineFlagValue(cmd_content, "-daemon"))
assetc::DaemonMode();
}
}
//
assetc::OutputPerfReport();
std::cout << "Exit" << std::endl;
return 0;
}