#include "asset/AssetCooker.h" #include "asset/AssetMigration.h" #include "asset/CanonicalImageCook.h" #include "asset/CanonicalPcgCook.h" #include "asset/EvpackCompression.h" #include "asset/RuntimeDefinition.h" #include "data/HashFunction.h" #include #include #include namespace eve::asset { namespace { template Result cookFailure(DiagnosticCode code, std::string message, std::string path = {}) { return Result::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, "asset.cook")); } std::array sha256(std::span bytes) { data::HashFunction::Value digest{}; data::HashFunction::getHashFunction("sha256") ->hash("sha256", reinterpret_cast(bytes.data()), bytes.size(), digest); std::array result{}; std::copy_n(reinterpret_cast(digest.data), result.size(), result.begin()); return result; } void appendString(std::vector& bytes, std::string_view text) { const std::uint64_t size = text.size(); for (unsigned shift = 0; shift != 64; shift += 8) bytes.push_back(static_cast(size >> shift)); bytes.insert(bytes.end(), text.begin(), text.end()); } PersistentId buildIdentity(const EvaArchive& source, const AssetCookProfile& profile, std::string_view canonicalManifest) { std::vector key; appendString(key, "eve.asset-cooker/1"); appendString(key, evpackCodecBuildIdentity(profile.chunkCodec)); const auto packageBytes = source.manifest.packageId.bytes(); key.insert(key.end(), packageBytes.begin(), packageBytes.end()); appendString(key, canonicalManifest); appendString(key, profile.variant.os); appendString(key, profile.variant.arch); appendString(key, profile.variant.graphics); appendString(key, profile.variant.shaderFormat); appendString(key, profile.variant.quality); for (const auto& family : profile.variant.textureFamilies) appendString(key, family); for (const auto& feature : profile.variant.features) appendString(key, feature); for (unsigned shift = 0; shift != 32; shift += 8) key.push_back(static_cast(profile.bulkAlignment >> shift)); for (const auto& entry : source.entries) { appendString(key, entry.path); const auto hash = sha256(entry.bytes); key.insert(key.end(), hash.begin(), hash.end()); } auto digest = sha256(key); PersistentId::Bytes bytes{}; std::copy_n(digest.begin(), bytes.size(), bytes.begin()); bytes[6] = static_cast((bytes[6] & 0x0f) | 0x50); bytes[8] = static_cast((bytes[8] & 0x3f) | 0x80); return PersistentId(bytes); } const Value* field(const Value::Object& object, std::string_view name) { const auto found = object.find(std::string(name)); return found == object.end() ? nullptr : &found->second; } bool predicateText(const Value::Object& predicate, std::string_view name, std::string_view actual) { const Value* value = field(predicate, name); return !value || (value->isString() && value->asString() == actual); } bool platformDependencyApplies(const EvaDependency& dependency, const EvpackVariant& variant) { if (dependency.kind != EvaDependencyKind::Platform) return true; return predicateText(dependency.predicate, "os", variant.os) && predicateText(dependency.predicate, "arch", variant.arch) && predicateText(dependency.predicate, "graphics", variant.graphics) && predicateText(dependency.predicate, "quality", variant.quality); } Result validatePublication(const EvaManifest& manifest, CookPublication publication) { if (publication == CookPublication::LocalInspection) return Result::success(); const Value* license = field(manifest.provenance, "license"); const auto* object = license ? license->getIf() : nullptr; const Value* redistribution = object ? field(*object, "redistribution") : nullptr; if (!redistribution || !redistribution->isString() || (redistribution->asString() != "runtime-embedded" && redistribution->asString() != "project-only")) return Result::failure(Diagnostic::error( DiagnosticCode::PreconditionViolation, "public Cook requires an explicit redistribution policy", "$.provenance.license.redistribution", {}, "asset.cook")); return Result::success(); } EvpackChunkKind kindForPath(std::string_view path) { if (path.find("/stream/") != std::string_view::npos) return EvpackChunkKind::Stream; if (path.find("/shaders/") != std::string_view::npos) return EvpackChunkKind::Shader; return EvpackChunkKind::Bulk; } Result> binaryDefinition( std::span json, std::uint64_t maximumBytes, std::span dependencies = {}) { auto parsed = Value::fromJson(std::string_view( reinterpret_cast(json.data()), json.size())); if (!parsed) return Result>::failure(parsed.status()); auto* object = parsed.value().getIf(); if (!object) return cookFailure>( DiagnosticCode::ParseError, "runtime definition root must be an object"); Value::Array fallbacks; for (const auto& dependency : dependencies) { if (dependency.kind != EvaDependencyKind::RuntimeOptional) continue; Value::Object fallback; fallback["path"] = Value(dependency.path); fallback["to"] = Value(dependency.to.format()); fallback["expectedType"] = Value(dependency.expectedType); fallback["policy"] = Value(dependency.fallback); fallbacks.emplace_back(std::move(fallback)); } if (!fallbacks.empty()) (*object)["dependencyFallbacks"] = Value(std::move(fallbacks)); RuntimeDefinitionLimits limits; limits.maximumBytes = maximumBytes; return encodeRuntimeDefinition(parsed.value(), limits); } } // namespace Result assetCookProfileForTarget(std::string_view target) { AssetCookProfile profile; profile.publication = CookPublication::LocalInspection; if (target == "windows-x86_64-vulkan") profile.variant = {"windows", "x86_64", "vulkan", {"bc", "rgba8"}, "spirv-1.6", "high", {}}; else if (target == "linux-x86_64-vulkan") profile.variant = {"linux", "x86_64", "vulkan", {"bc", "rgba8"}, "spirv-1.6", "high", {}}; else if (target == "macos-arm64-vulkan") profile.variant = {"macos", "arm64", "vulkan", {"rgba8"}, "spirv-1.6", "high", {}}; else if (target == "android-arm64-vulkan") profile.variant = {"android", "arm64", "vulkan", {"astc", "etc2", "rgba8"}, "spirv-1.6", "high", {}}; else if (target == "ios-arm64-vulkan") profile.variant = {"ios", "arm64", "vulkan", {"astc", "rgba8"}, "spirv-1.6", "high", {}}; else if (target == "web-wasm32-webgpu") profile.variant = {"web", "wasm32", "webgpu", {"rgba8"}, "wgsl-1", "high", {}}; else return cookFailure(DiagnosticCode::Unsupported, "unknown asset Cook target", std::string(target)); return Result::success(std::move(profile)); } Result cookEvaToEvpack(const EvaArchive& source, const AssetCookProfile& profile, const EvaArchiveLimits& evaLimits, const EvpackLimits& evpackLimits) { auto migrated = migrateEvaArchive(source, evaLimits); if (!migrated) return Result::failure(migrated.status()); auto rebuilt = buildEvaArchive(migrated.value().manifest, migrated.value().entries, evaLimits); if (!rebuilt) return Result::failure(rebuilt.status()); auto admitted = parseEvaArchive(rebuilt.value(), evaLimits); if (!admitted) return Result::failure(admitted.status()); EvaArchive canonical = std::move(admitted).takeValue(); EvaManifest dependencyManifest = canonical.manifest; std::erase_if(dependencyManifest.dependencies, [&](const EvaDependency& dependency) { return dependency.kind == EvaDependencyKind::Platform && !platformDependencyApplies(dependency, profile.variant); }); auto dependencyValidation = validateEvaDependencies(dependencyManifest, profile.availableDependencies); if (!dependencyValidation) return Result::failure(dependencyValidation.status()); auto canonicalManifest = serializeEvaManifest(canonical.manifest); if (!canonicalManifest) return Result::failure(canonicalManifest.status()); auto publication = validatePublication(canonical.manifest, profile.publication); if (!publication) return Result::failure(publication.status()); std::map> runtimeDependencies; for (const auto& dependency : canonical.manifest.dependencies) { if (dependency.kind == EvaDependencyKind::RuntimeOptional || dependency.kind == EvaDependencyKind::Build || dependency.kind == EvaDependencyKind::Editor || dependency.kind == EvaDependencyKind::Source || !platformDependencyApplies(dependency, profile.variant)) continue; const bool present = std::binary_search(dependencyValidation.value().presentAssets.begin(), dependencyValidation.value().presentAssets.end(), dependency.to.id()); if (present) runtimeDependencies[dependency.from.id()].push_back(dependency.to.id()); } for (auto& [asset, dependencies] : runtimeDependencies) { (void)asset; std::sort(dependencies.begin(), dependencies.end()); dependencies.erase(std::unique(dependencies.begin(), dependencies.end()), dependencies.end()); } EvpackBuild build; build.packageId = canonical.manifest.packageId; build.buildId = buildIdentity(canonical, profile, canonicalManifest.value()); build.variants.push_back(profile.variant); for (const auto& asset : canonical.manifest.assets) { std::vector assetDependencyPolicies; for (const auto& dependency : canonical.manifest.dependencies) if (dependency.from.id() == asset.asset.id() && platformDependencyApplies(dependency, profile.variant)) assetDependencyPolicies.push_back(dependency); const auto definition = std::lower_bound(canonical.entries.begin(), canonical.entries.end(), asset.definition, [](const EvaArchiveEntry& entry, std::string_view path) { return entry.path < path; }); if (definition == canonical.entries.end() || definition->path != asset.definition) return cookFailure(DiagnosticCode::NotFound, "asset definition is missing", asset.definition); const auto dependencies = runtimeDependencies[asset.asset.id()]; if (asset.type == "eve.image") { if (std::find(profile.variant.textureFamilies.begin(), profile.variant.textureFamilies.end(), "rgba8") == profile.variant.textureFamilies.end()) return cookFailure( DiagnosticCode::Unsupported, "eve.image/2 requires an explicit rgba8 target texture family", asset.definition); auto parsedDefinition = Value::fromJson(std::string_view( reinterpret_cast(definition->bytes.data()), definition->bytes.size())); if (!parsedDefinition) return Result::failure(parsedDefinition.status()); const auto* definitionObject = parsedDefinition.value().getIf(); const Value* blobValue = definitionObject ? field(*definitionObject, "blob") : nullptr; if (!blobValue || !blobValue->isString()) return cookFailure(DiagnosticCode::ParseError, "eve.image/2 source blob path is missing", asset.definition); const auto sourceBlob = std::lower_bound( canonical.entries.begin(), canonical.entries.end(), blobValue->asString(), [](const EvaArchiveEntry& entry, std::string_view path) { return entry.path < path; }); if (sourceBlob == canonical.entries.end() || sourceBlob->path != blobValue->asString()) return cookFailure(DiagnosticCode::NotFound, "eve.image/2 source blob is missing", blobValue->asString()); auto image = cookCanonicalImageRgba8(definition->bytes, sourceBlob->bytes, evpackLimits.maximumChunkBytes); if (!image) return Result::failure(image.status()); auto runtimeDefinition = binaryDefinition(image.value().definition, evpackLimits.maximumChunkBytes, assetDependencyPolicies); if (!runtimeDefinition) return Result::failure(runtimeDefinition.status()); build.chunks.push_back({asset.asset.id(), asset.type, asset.schemaVersion, 0, EvpackChunkKind::Definition, 0, profile.chunkCodec, 8, dependencies, std::move(runtimeDefinition).takeValue()}); build.chunks.push_back({asset.asset.id(), asset.type, asset.schemaVersion, 0, EvpackChunkKind::Bulk, 1, EvpackCodec::None, profile.bulkAlignment, dependencies, std::move(image).takeValue().bulk}); continue; } if (asset.type == "eve.pcg-graph") { auto pcg = cookCanonicalPcgGraph(definition->bytes, 4096, evpackLimits.maximumChunkBytes); if (!pcg) return Result::failure(pcg.status()); auto runtimeDefinition = binaryDefinition(pcg.value().definition, evpackLimits.maximumChunkBytes, assetDependencyPolicies); if (!runtimeDefinition) return Result::failure(runtimeDefinition.status()); build.chunks.push_back({asset.asset.id(), asset.type, asset.schemaVersion, 0, EvpackChunkKind::Definition, 0, profile.chunkCodec, 8, dependencies, std::move(runtimeDefinition).takeValue()}); build.chunks.push_back({asset.asset.id(), asset.type, asset.schemaVersion, 0, EvpackChunkKind::Bulk, 1, profile.chunkCodec, profile.bulkAlignment, dependencies, std::move(pcg).takeValue().executionPlan}); continue; } auto runtimeDefinition = binaryDefinition(definition->bytes, evpackLimits.maximumChunkBytes, assetDependencyPolicies); if (!runtimeDefinition) return Result::failure(runtimeDefinition.status()); build.chunks.push_back({asset.asset.id(), asset.type, asset.schemaVersion, 0, EvpackChunkKind::Definition, 0, profile.chunkCodec, 8, dependencies, std::move(runtimeDefinition).takeValue()}); const std::string prefix = "assets/" + asset.asset.id().format() + "/"; std::uint32_t chunkId = 1; for (const auto& entry : canonical.entries) { if (entry.path == asset.definition || !entry.path.starts_with(prefix)) continue; build.chunks.push_back({asset.asset.id(), asset.type, asset.schemaVersion, 0, kindForPath(entry.path), chunkId++, profile.chunkCodec, profile.bulkAlignment, dependencies, entry.bytes}); } } const std::uint32_t chunkCount = static_cast(build.chunks.size()); const PersistentId buildId = build.buildId; auto packageBytes = buildEvpack(std::move(build), evpackLimits); if (!packageBytes) return Result::failure(packageBytes.status()); auto verified = parseEvpack(packageBytes.value(), evpackLimits); if (!verified) return Result::failure(verified.status()); return Result::success( {canonical.manifest.packageId, buildId, chunkCount, std::move(packageBytes).takeValue()}); } } // namespace eve::asset