diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 12b0534..775096e 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -1098,24 +1098,39 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) { // Walk the dep graph again for non-JS assets — these get pre-loaded by // 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. 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 { + // stubs every fd syscall to zero). + // + // The manifest lists *relative paths* (e.g. "assets/Inter.ttf") so + // runtime.js's fetch() resolves against the bin-dir layout the asset + // copy step actually emits. The VFS is keyed by basename — path_open + // strips to basename on lookup, so subdir layouts collapse on the + // wasm side. Basename collisions across subdirs aren't supported on + // the wasi runtime today; if two assets share a basename, the last + // one preloaded wins. Avoid collisions in the source tree. + auto compressedExt = [](const fs::path& src) -> std::optional { std::string ext = src.extension().string(); for (char& c : ext) c = static_cast(std::tolower(static_cast(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 std::string(".ctex"); } - return out.string(); + if (ext == ".obj") return std::string(".cmesh"); + return std::nullopt; + }; + auto compressedRel = [&](const fs::path& rel) -> fs::path { + if (auto ext = compressedExt(rel)) { + fs::path out = rel; + out.replace_extension(*ext); + return out; + } + return rel; }; std::vector assetFiles; + auto pushUnique = [&](std::string name) { + if (name.empty()) return; + if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) { + assetFiles.push_back(std::move(name)); + } + }; seen.clear(); std::function walkAssets = [&](Configuration* c) { if (!c || !seen.insert(c).second) return; @@ -1123,35 +1138,25 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) { std::string ext = f.extension().string(); if (ext == ".js" || ext == ".html") continue; if (f.filename() == "runtime.js") continue; - std::string name = f.filename().string(); - if (name.empty()) continue; - if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) { - assetFiles.push_back(std::move(name)); - } + // cfg.files lands flat next to the .wasm by `name = filename()`. + pushUnique(f.filename().string()); } - // 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). + // cfg.assets — mirror the bin-dir layout the build emits: a + // directory entry becomes /, single + // files land flat at the bin root. .png/.obj are compressed in + // place; everything else passes through under its original name. for (const fs::path& a : c->assets) { if (fs::is_directory(a)) { + const fs::path topName = a.filename(); 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)); - } + fs::path rel = fs::relative(entry.path(), a); + pushUnique((topName / compressedRel(rel)).generic_string()); } } else { - std::optional name = compressedBasename(a); - if (!name) continue; - if (std::find(assetFiles.begin(), assetFiles.end(), *name) == assetFiles.end()) { - assetFiles.push_back(std::move(*name)); - } + pushUnique(compressedRel(a.filename()).generic_string()); } } for (Configuration* dep : c->dependencies) walkAssets(dep); diff --git a/wasi-runtime/runtime.js b/wasi-runtime/runtime.js index ceaae77..2ce232c 100644 --- a/wasi-runtime/runtime.js +++ b/wasi-runtime/runtime.js @@ -301,6 +301,12 @@ if (!wasmUrl) { // EnableWasiBrowserRuntime) into an in-memory VFS so wasi-libc's file // syscalls work against them. Browser builds otherwise can't open assets // shipped alongside the .wasm — sync XHR is too deprecated to rely on. +// +// Manifest entries are relative paths under the bin dir (the layout +// `cfg.assets` produces — e.g. "assets/Inter.ttf", +// "mods/3DForts_Base/foo.cmesh"). We fetch each at its full path but +// key the VFS by basename so `path_open`'s basename-reduction can find +// it regardless of the C++ side's cwd or prefix. const vfs = new Map(); try { const manifestResp = await fetch("files.json"); @@ -308,8 +314,12 @@ try { const names = await manifestResp.json(); await Promise.all(names.map(async (name) => { const r = await fetch(name); - if (r.ok) vfs.set(name, new Uint8Array(await r.arrayBuffer())); - else console.warn(`[wasi] failed to preload ${name}: HTTP ${r.status}`); + if (!r.ok) { + console.warn(`[wasi] failed to preload ${name}: HTTP ${r.status}`); + return; + } + const base = name.split(/[\\/]/).filter(Boolean).pop() || name; + vfs.set(base, new Uint8Array(await r.arrayBuffer())); })); } } catch (e) {