This commit is contained in:
parent
c466d90eec
commit
f442caa888
6 changed files with 224 additions and 11 deletions
|
|
@ -29,9 +29,15 @@ namespace fs = std::filesystem;
|
|||
namespace Crafter {
|
||||
std::string CompressAsset(const fs::path& input, const fs::path& output) {
|
||||
std::string ext = input.extension().string();
|
||||
// Case-insensitive extension match — Sponza's textures dir mixes
|
||||
// case (.tga and .TGA both appear in the wild).
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
#ifdef CRAFTER_BUILD_HAS_ASSET
|
||||
try {
|
||||
if (ext == ".png") {
|
||||
// stb_image (the loader behind LoadPNG) handles all of these,
|
||||
// so a single branch covers every raster format we ship. The
|
||||
// function is misnamed for historical reasons.
|
||||
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
|
||||
auto tex = TextureAsset<Vector<std::uint8_t, 4, 4>>::LoadPNG<std::uint8_t>(input);
|
||||
tex.SaveCompressed(output);
|
||||
} else if (ext == ".obj") {
|
||||
|
|
@ -50,6 +56,65 @@ namespace Crafter {
|
|||
"rebuild via `./bin/crafter-build` so the self-host pass picks up the "
|
||||
"Crafter.Asset dep declared in project.cpp",
|
||||
input.string());
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string BuildOBJBundle(
|
||||
const fs::path& objPath,
|
||||
const fs::path& mtlPath,
|
||||
const fs::path& outDir,
|
||||
std::uint16_t albedoSize)
|
||||
{
|
||||
#ifdef CRAFTER_BUILD_HAS_ASSET
|
||||
const fs::path manifest = outDir / "scene.txt";
|
||||
if (fs::exists(manifest)) return {};
|
||||
try {
|
||||
fs::create_directories(outDir);
|
||||
auto materials = LoadMTL(mtlPath);
|
||||
auto meshes = MeshAsset<VertexNormalTangentUVPacked>::LoadOBJSplit(objPath);
|
||||
|
||||
std::vector<std::string> uniqueAlbedos;
|
||||
std::unordered_map<std::string, std::uint32_t> albedoIndex;
|
||||
for (auto& [matName, _] : meshes) {
|
||||
auto matIt = materials.find(matName);
|
||||
if (matIt == materials.end() || matIt->second.mapKd.empty()) continue;
|
||||
const std::string& kd = matIt->second.mapKd;
|
||||
if (!albedoIndex.contains(kd)) {
|
||||
albedoIndex.emplace(kd, static_cast<std::uint32_t>(uniqueAlbedos.size()));
|
||||
uniqueAlbedos.push_back(kd);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::int32_t> meshAlbedoIdx;
|
||||
std::uint32_t emitted = 0;
|
||||
for (auto& [matName, mesh] : meshes) {
|
||||
if (mesh.vertexes.empty() || mesh.indexes.empty()) continue;
|
||||
mesh.SaveCompressed(outDir / std::format("mesh_{}.cmesh", emitted));
|
||||
std::int32_t a = -1;
|
||||
if (auto it = materials.find(matName);
|
||||
it != materials.end() && !it->second.mapKd.empty())
|
||||
a = static_cast<std::int32_t>(albedoIndex.at(it->second.mapKd));
|
||||
meshAlbedoIdx.push_back(a);
|
||||
++emitted;
|
||||
}
|
||||
|
||||
for (std::uint32_t i = 0; i < uniqueAlbedos.size(); ++i) {
|
||||
const fs::path texPath = mtlPath.parent_path() / uniqueAlbedos[i];
|
||||
auto tex = TextureAsset<Vector<std::uint8_t, 4, 4>>::LoadPNG<std::uint8_t>(texPath);
|
||||
tex.Resize(albedoSize, albedoSize);
|
||||
tex.SaveCompressed(outDir / std::format("tex_{}.ctex", i));
|
||||
}
|
||||
|
||||
std::ofstream m(manifest);
|
||||
m << uniqueAlbedos.size() << "\n" << emitted << "\n";
|
||||
for (std::int32_t a : meshAlbedoIdx) m << a << "\n";
|
||||
} catch (const std::exception& e) {
|
||||
return std::format("BuildOBJBundle: {}", e.what());
|
||||
}
|
||||
return {};
|
||||
#else
|
||||
return "BuildOBJBundle: crafter-build was bootstrapped without Crafter.Asset linkage; "
|
||||
"rebuild via crafter-build itself to pick up the Crafter.Asset dep";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ 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;
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
export module Crafter.Build:Clang_impl;
|
||||
import std;
|
||||
import :Clang;
|
||||
|
|
@ -304,7 +313,12 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// a single thread to match the cfg.files pattern.
|
||||
auto compressedName = [](const fs::path& src) -> std::optional<fs::path> {
|
||||
std::string ext = src.extension().string();
|
||||
if (ext == ".png") return fs::path(src.filename()).replace_extension(".ctex");
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
// stb_image (used by CompressAsset → TextureAsset::LoadPNG) handles
|
||||
// png/tga/jpg/bmp; all map to .ctex.
|
||||
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
|
||||
return fs::path(src.filename()).replace_extension(".ctex");
|
||||
}
|
||||
if (ext == ".obj") return fs::path(src.filename()).replace_extension(".cmesh");
|
||||
return std::nullopt;
|
||||
};
|
||||
|
|
@ -341,7 +355,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
std::optional<fs::path> outName = compressedName(asset);
|
||||
if (!outName) {
|
||||
buildCancelled.store(true);
|
||||
buildError = std::format("{}: unsupported asset extension (expected .png or .obj, or a directory)", asset.string());
|
||||
buildError = std::format("{}: unsupported asset extension (expected .png/.tga/.jpg/.bmp/.obj, or a directory)", asset.string());
|
||||
break;
|
||||
}
|
||||
submitCompress(asset, *outName);
|
||||
|
|
@ -752,10 +766,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
continue;
|
||||
}
|
||||
std::string ext = asset.extension().string();
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
fs::path srcName = asset.filename();
|
||||
if (ext == ".png") srcName.replace_extension(".ctex");
|
||||
else if (ext == ".obj") srcName.replace_extension(".cmesh");
|
||||
else continue;
|
||||
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
|
||||
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);
|
||||
|
|
@ -1080,7 +1099,22 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
// runtime.js into an in-memory VFS so the wasm's std::ifstream et al.
|
||||
// can actually read them (the wasi-runtime in this repo otherwise
|
||||
// stubs every fd syscall to zero). The manifest lists basenames only;
|
||||
// each file lives next to the .wasm in the bin dir.
|
||||
// each file lives next to the .wasm in the bin dir. wasi-runtime
|
||||
// reduces every path_open path to its basename, so subdir layouts
|
||||
// collapse — that's fine for our flat-bin convention.
|
||||
auto compressedBasename = [](const fs::path& src) -> std::optional<std::string> {
|
||||
std::string ext = src.extension().string();
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
fs::path out = src.filename();
|
||||
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
|
||||
out.replace_extension(".ctex");
|
||||
} else if (ext == ".obj") {
|
||||
out.replace_extension(".cmesh");
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
return out.string();
|
||||
};
|
||||
std::vector<std::string> assetFiles;
|
||||
seen.clear();
|
||||
std::function<void(Configuration*)> walkAssets = [&](Configuration* c) {
|
||||
|
|
@ -1095,6 +1129,31 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
assetFiles.push_back(std::move(name));
|
||||
}
|
||||
}
|
||||
// cfg.assets — emit the *compressed* output basename. Directory
|
||||
// entries get the same compressed-basename treatment per file;
|
||||
// unrecognized extensions are treated as passthrough (kept under
|
||||
// their original name, matching the build-side passthrough copy).
|
||||
for (const fs::path& a : c->assets) {
|
||||
if (fs::is_directory(a)) {
|
||||
std::error_code ec;
|
||||
for (const auto& entry : fs::recursive_directory_iterator(a, ec)) {
|
||||
if (ec) break;
|
||||
if (!entry.is_regular_file()) continue;
|
||||
std::string name = compressedBasename(entry.path())
|
||||
.value_or(entry.path().filename().string());
|
||||
if (name.empty()) continue;
|
||||
if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) {
|
||||
assetFiles.push_back(std::move(name));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::optional<std::string> name = compressedBasename(a);
|
||||
if (!name) continue;
|
||||
if (std::find(assetFiles.begin(), assetFiles.end(), *name) == assetFiles.end()) {
|
||||
assetFiles.push_back(std::move(*name));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Configuration* dep : c->dependencies) walkAssets(dep);
|
||||
};
|
||||
walkAssets(&cfg);
|
||||
|
|
@ -1339,10 +1398,35 @@ int Crafter::Run(int argc, char** argv) {
|
|||
return std::system(probe.c_str()) == 0;
|
||||
};
|
||||
if (browserBuild) {
|
||||
// Try installed HTTP servers in priority order: lightweight
|
||||
// / dependency-free first, ad-hoc ones last. Foreground
|
||||
// (Ctrl-C to stop); we exec, not fork.
|
||||
const int port = 8080;
|
||||
// Probe-bind to find a free port starting at 8080 — if the
|
||||
// user has another dev server running we shift up rather
|
||||
// than letting caddy/python exit with EADDRINUSE.
|
||||
auto findFreePort = [](int basePort, int span) -> int {
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
static const bool wsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }();
|
||||
if (!wsaInit) return basePort;
|
||||
using sock_t = SOCKET;
|
||||
const sock_t invalid = INVALID_SOCKET;
|
||||
auto closesock = [](sock_t s){ closesocket(s); };
|
||||
#else
|
||||
using sock_t = int;
|
||||
const sock_t invalid = -1;
|
||||
auto closesock = [](sock_t s){ ::close(s); };
|
||||
#endif
|
||||
for (int p = basePort; p < basePort + span; ++p) {
|
||||
sock_t s = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s == invalid) return basePort;
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(p));
|
||||
bool ok = ::bind(s, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0;
|
||||
closesock(s);
|
||||
if (ok) return p;
|
||||
}
|
||||
return basePort;
|
||||
};
|
||||
const int port = findFreePort(8080, 16);
|
||||
std::string cmd;
|
||||
std::string_view picked;
|
||||
if (have("caddy")) {
|
||||
|
|
|
|||
|
|
@ -396,6 +396,34 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
|||
return ptr;
|
||||
}
|
||||
|
||||
fs::path Crafter::GitFetch(const GitSource& source) {
|
||||
std::string name = DeriveName(source);
|
||||
if (name.empty()) {
|
||||
throw std::runtime_error(std::format(
|
||||
"GitFetch: could not derive name from URL '{}'", source.url));
|
||||
}
|
||||
fs::path externalRoot = GetCacheDir() / "external";
|
||||
std::error_code ec;
|
||||
fs::create_directories(externalRoot, ec);
|
||||
if (ec) {
|
||||
throw std::runtime_error(std::format(
|
||||
"GitFetch: failed to create {}: {}", externalRoot.string(), ec.message()));
|
||||
}
|
||||
std::string keyMaterial = std::format("git|{}|{}|{}",
|
||||
source.url, source.branch, source.commit);
|
||||
std::size_t key = std::hash<std::string>{}(keyMaterial);
|
||||
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
|
||||
|
||||
{
|
||||
bool exists = fs::exists(cloneDir);
|
||||
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
||||
if (std::string err = FetchGit(source, cloneDir); !err.empty()) {
|
||||
throw std::runtime_error(std::format("GitFetch({}): {}", source.url, err));
|
||||
}
|
||||
}
|
||||
return cloneDir;
|
||||
}
|
||||
|
||||
Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) {
|
||||
fs::path projectPath = fs::absolute(spec.projectFile).lexically_normal();
|
||||
if (!fs::exists(projectPath)) {
|
||||
|
|
|
|||
|
|
@ -36,4 +36,26 @@ namespace Crafter {
|
|||
// support requires a self-host rebuild via crafter-build itself,
|
||||
// which resolves the Crafter.Asset dep through project.cpp.
|
||||
export CRAFTER_API std::string CompressAsset(const fs::path& input, const fs::path& output);
|
||||
|
||||
// Split a single .obj + .mtl file by material group into per-material
|
||||
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
|
||||
// under outDir. All albedo textures are resized to albedoSize × albedoSize
|
||||
// before compression (required for a texture_2d_array where all layers
|
||||
// must have identical dimensions).
|
||||
//
|
||||
// scene.txt format (read by the Sponza example at runtime):
|
||||
// <albedoCount>
|
||||
// <meshCount>
|
||||
// <albedoIdx_for_mesh_0> (-1 = no albedo)
|
||||
// ...
|
||||
//
|
||||
// Idempotent: returns "" immediately if outDir/scene.txt already exists,
|
||||
// so repeated builds skip the expensive split+resize step.
|
||||
// Returns "" on success, an error string on failure.
|
||||
export CRAFTER_API std::string BuildOBJBundle(
|
||||
const fs::path& objPath,
|
||||
const fs::path& mtlPath,
|
||||
const fs::path& outDir,
|
||||
std::uint16_t albedoSize = 1024
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,4 +97,14 @@ export namespace Crafter {
|
|||
// recursively LoadProject's it. Returns a stable Configuration* owned
|
||||
// by the same internal cache as GitProject.
|
||||
CRAFTER_API Configuration* LocalProject(const LocalProjectSpec& spec);
|
||||
|
||||
// Clone (or update) the given git source into the shared external
|
||||
// cache and return the path to the cloned working tree. Intended for
|
||||
// example projects that need large asset bundles which shouldn't
|
||||
// live in the parent repo — push the returned path (or specific
|
||||
// files inside it) into `cfg.assets`. Reuses the same cache root as
|
||||
// ExternalDependency / GitProject. Caching key = url + branch + commit;
|
||||
// identical calls reuse the existing clone. Pin `source.commit` to a
|
||||
// SHA for reproducible, offline-after-first-fetch builds.
|
||||
CRAFTER_API fs::path GitFetch(const GitSource& source);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
cfg.linkFlags.push_back("-ldl");
|
||||
}
|
||||
if (cfg.target == "x86_64-w64-mingw32" || cfg.target == "x86_64-pc-windows-msvc") {
|
||||
// winsock for the -r wasm port probe (bind/WSAStartup).
|
||||
crafterBuildLib->linkFlags.push_back("-lws2_32");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue