Rebuild local Blender WASM environment and recovery evidence
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-27 20:15:55 +08:00
parent a081c68c87
commit c2c65d3a09
376 changed files with 241951 additions and 114 deletions

View File

@@ -757,7 +757,10 @@ bool capture_history_entry(const EngineState &engine, HistoryEntry &entry, std::
return true;
}
bool restore_history_entry(EngineState &engine, const HistoryEntry &entry, std::string &error)
bool restore_history_entry(EngineState &engine,
const HistoryEntry &entry,
std::string &error,
const bool advance_revision = false)
{
json snapshot;
try {
@@ -777,7 +780,7 @@ bool restore_history_entry(EngineState &engine, const HistoryEntry &entry, std::
if (restored_main == nullptr) {
return false;
}
snapshot["revision"] = engine.revision + 1;
snapshot["revision"] = engine.revision + (advance_revision ? 1 : 0);
const std::string restored_snapshot = snapshot.dump();
const std::string previous_snapshot = engine.scene_snapshot.empty() ?
std::string(empty_scene_snapshot_json()) :
@@ -800,7 +803,7 @@ bool restore_history_entry(EngineState &engine, const HistoryEntry &entry, std::
engine.authoritative_dirty = false;
engine.scene_snapshot = restored_snapshot;
engine.scene_delta = std::move(delta);
engine.revision++;
if (advance_revision) engine.revision++;
return true;
}
@@ -1352,7 +1355,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("showRender", true), command.value("showEditMode", false),
command.value("showOnCage", false), main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot.value("activeObjectId", ""), main_error)) {
restore_history_entry(*engine, previous_state, history_error);
restore_history_entry(*engine, previous_state, history_error, false);
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
@@ -2314,7 +2317,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_undo(const int handle)
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, history_error.c_str());
return last_error_code;
}
if (!restore_history_entry(*engine, engine->undo_history.back(), history_error)) {
if (!restore_history_entry(*engine, engine->undo_history.back(), history_error, true)) {
set_error(WEB_ENGINE_BLEND_READ_FAILED, history_error.c_str());
return last_error_code;
}
@@ -2338,7 +2341,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_redo(const int handle)
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, history_error.c_str());
return last_error_code;
}
if (!restore_history_entry(*engine, engine->redo_history.back(), history_error)) {
if (!restore_history_entry(*engine, engine->redo_history.back(), history_error, true)) {
set_error(WEB_ENGINE_BLEND_READ_FAILED, history_error.c_str());
return last_error_code;
}

View File

@@ -2231,6 +2231,690 @@ std::optional<json> script_source_from_record(const ParsedBlend &blend,
return result;
}
/* The capability contract is carried by one read-only Text datablock so the
* desktop and WASM readers consume identical canonical bytes. Unknown
* extension fields stay in the JSON object and are never rewritten here. */
std::optional<json> capability_contract_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P0-003-CapabilityContract") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const std::string text = source->value("source", std::string());
if (text.empty() || text.size() > 65536) return std::nullopt;
const json contract = json::parse(text, nullptr, false);
if (contract.is_discarded() || !contract.is_object() ||
contract.value("schemaVersion", 0) != 1 ||
contract.value("contractId", std::string()) !=
"workflow:freeze_capability_error_budget_contract" ||
!contract.contains("capability") || !contract.contains("revision") ||
!contract.contains("budget") || !contract.contains("errors") ||
!contract.contains("cancellation") || !contract.contains("migration") ||
!contract.contains("schemaHash"))
{
return std::nullopt;
}
if (!contract["capability"].is_object() || !contract["revision"].is_object() ||
!contract["budget"].is_object() || !contract["errors"].is_array() ||
contract["errors"].empty() || contract["errors"].size() > 128 ||
!contract["cancellation"].is_object() || !contract["migration"].is_object() ||
!contract["schemaHash"].is_string() ||
contract["schemaHash"].get<std::string>().size() != 64)
{
return std::nullopt;
}
const int64_t max_errors = contract["budget"].value("maxErrors", int64_t(0));
const int64_t max_unknown_fields = contract["budget"].value("maxUnknownFields", int64_t(-1));
const int64_t max_message_bytes = contract["budget"].value("maxMessageBytes", int64_t(0));
if (max_errors < 1 || max_errors > 128 || max_unknown_fields < 0 ||
max_unknown_fields > 128 || max_message_bytes < 32 || max_message_bytes > 4096 ||
contract["errors"].size() > size_t(max_errors) ||
contract["cancellation"].value("code", std::string()) != "OPEN_CANCELLED" ||
!contract["cancellation"].value("preservesRevision", false) ||
contract["migration"].value("currentSchemaVersion", 0) != 1 ||
contract["migration"].value("preserveUnknown", false) != true ||
contract["migration"].value("strategy", std::string()) != "RETAIN_UNKNOWN")
{
return std::nullopt;
}
std::unordered_set<std::string> error_codes;
for (const json &error : contract["errors"]) {
if (!error.is_object() || !error.contains("code") || !error["code"].is_string() ||
error["code"].get<std::string>().empty() || !error.contains("severity") ||
!error["severity"].is_string() || error["severity"].get<std::string>().empty() ||
!error.contains("recoverable") || !error["recoverable"].is_boolean())
{
return std::nullopt;
}
const std::string code = error.value("code", std::string());
if (code.empty() || !error_codes.insert(code).second) return std::nullopt;
}
json schema_input = contract;
schema_input.erase("schemaHash");
if (sha256_hex(schema_input.dump()) != contract["schemaHash"].get<std::string>()) {
return std::nullopt;
}
return contract;
}
/* Render routing is a read-only receipt embedded in the fixture so desktop and
* WASM consume the same settings/build identity and fail-closed decisions. */
std::optional<json> render_routing_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P5-001-RenderRoutingReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const json receipt = json::parse(source->value("source", std::string()), nullptr, false);
if (receipt.is_discarded() || !receipt.is_object() || receipt.value("schemaVersion", 0) != 1 ||
receipt.value("receiptId", std::string()) != "workflow:render_capability_routing" ||
!receipt.contains("source") || !receipt.contains("settings") ||
!receipt.contains("build") || !receipt.contains("routes") ||
!receipt.contains("recovery"))
{
return std::nullopt;
}
const auto digest = [](const json &value, const char *field) {
if (!value.contains(field) || !value[field].is_string()) return false;
const std::string text = value[field].get<std::string>();
return text.size() == 64 && std::all_of(text.begin(), text.end(), [](const char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
};
if (!receipt["source"].is_object() || !digest(receipt["source"], "sha256") ||
!receipt["settings"].is_object() || !digest(receipt["settings"], "sha256") ||
!receipt["build"].is_object() || !digest(receipt["build"], "buildSha256") ||
!receipt["routes"].is_array() || receipt["routes"].empty() ||
receipt["routes"].size() > 32 || !receipt["recovery"].is_object())
{
return std::nullopt;
}
for (const json &route : receipt["routes"]) {
if (!route.is_object() || !route.contains("name") || !route["name"].is_string() ||
!route.contains("target") || !route["target"].is_string() ||
!route.contains("status") || !route["status"].is_string() ||
(route["status"] != "READY" && route["status"] != "BLOCKED"))
{
return std::nullopt;
}
}
if (receipt["recovery"].value("cancellation", std::string()) != "OPEN_CANCELLED" ||
receipt["recovery"].value("deviceLoss", std::string()) != "RELEASED")
{
return std::nullopt;
}
return receipt;
}
std::optional<json> server_job_binding_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P5-003-ServerJobBindingReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const json receipt = json::parse(source->value("source", std::string()), nullptr, false);
if (receipt.is_discarded() || !receipt.is_object() || receipt.value("schemaVersion", 0) != 1 ||
receipt.value("receiptId", std::string()) != "workflow:server_job_result_binding" ||
!receipt.contains("source") || !receipt.contains("settings") ||
!receipt.contains("build") || !receipt.contains("request") ||
!receipt.contains("result") || !receipt.contains("lifecycle"))
{
return std::nullopt;
}
const auto digest = [](const json &value, const char *field) {
if (!value.contains(field) || !value[field].is_string()) return false;
const std::string text = value[field].get<std::string>();
return text.size() == 64 && std::all_of(text.begin(), text.end(), [](const char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
};
if (!receipt["source"].is_object() || !digest(receipt["source"], "sha256") ||
!receipt["settings"].is_object() || !digest(receipt["settings"], "settingsSha256") ||
!receipt["build"].is_object() || !digest(receipt["build"], "buildSha256") ||
!receipt["request"].is_object() || !digest(receipt["request"], "requestSha256") ||
!receipt["result"].is_object() || !digest(receipt["result"], "resultSha256") ||
!receipt["lifecycle"].is_object())
{
return std::nullopt;
}
const std::string result_status = receipt["result"].value("status", std::string());
if (result_status != "SUCCEEDED" ||
receipt["lifecycle"].value("duplicate", std::string()) != "REUSED" ||
receipt["lifecycle"].value("stale", std::string()) != "BLOCKED" ||
receipt["lifecycle"].value("cancel", std::string()) != "CANCELLED" ||
receipt["lifecycle"].value("timeout", std::string()) != "TIMEOUT" ||
receipt["lifecycle"].value("oom", std::string()) != "OOM" ||
receipt["lifecycle"].value("publishedOnFailure", true))
{
return std::nullopt;
}
return receipt;
}
/* Scripting policy is persisted as a read-only receipt. The reader validates
* the default-deny contract and its integrity hash but never evaluates the
* Text source or grants a host capability. */
std::optional<json> scripting_policy_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P6-001-ScriptingPolicyReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const json policy = json::parse(source->value("source", std::string()), nullptr, false);
if (policy.is_discarded() || !policy.is_object() || policy.value("schemaVersion", 0) != 1 ||
policy.value("policyId", std::string()) != "workflow:scripting_default_deny_audit" ||
!policy.contains("defaults") || !policy.contains("scope") || !policy.contains("sandbox") ||
!policy.contains("audit") || !policy.contains("boundaries") || !policy.contains("budget") ||
!policy.contains("policyHash"))
{
return std::nullopt;
}
for (const char *field : {"defaults", "scope", "sandbox", "audit", "boundaries", "budget"}) {
if (!policy[field].is_object()) return std::nullopt;
}
const json &defaults = policy["defaults"];
for (const char *field : {"autorun", "network", "addonInstall", "driverExpressions", "module"}) {
if (!defaults.contains(field) || defaults[field] != false) return std::nullopt;
}
const json &scope = policy["scope"];
for (const char *field : {"dom", "hostWorker", "opfs", "indexedDB", "network"}) {
if (!scope.contains(field) || scope[field] != false) return std::nullopt;
}
const json &sandbox = policy["sandbox"];
if (sandbox.value("execution", std::string()) != "DISABLED" ||
sandbox.value("cancellation", std::string()) != "SCRIPT_SANDBOX_CANCELLED" ||
sandbox.value("dispose", std::string()) != "RELEASED" ||
!sandbox.value("mainRevisionUnchanged", false) ||
sandbox.value("publishedResults", int64_t(-1)) != 0 ||
sandbox.value("temporaryBytes", int64_t(-1)) != 0)
{
return std::nullopt;
}
const json &audit = policy["audit"];
if (audit.value("decision", std::string()) != "DENY" ||
audit.value("approvedKey", true) ||
!((audit.value("reason", std::string()) == "SCRIPT_SIGNATURE_INVALID") ||
(audit.value("reason", std::string()) == "SCRIPT_SANDBOX_UNAVAILABLE")))
{
return std::nullopt;
}
const json &boundaries = policy["boundaries"];
for (const char *field : {"secretAccess", "filesystemEscape", "network"}) {
if (boundaries.value(field, std::string()) != "BLOCKED") return std::nullopt;
}
const json &budget = policy["budget"];
if (budget.value("maxSourceBytes", int64_t(0)) < 1 ||
budget.value("maxSourceBytes", int64_t(0)) > int64_t(MAX_SCRIPT_SOURCE_BYTES) ||
budget.value("maxScripts", int64_t(0)) < 1 || budget.value("maxScripts", int64_t(0)) > 1024 ||
budget.value("maxAuditEntries", int64_t(0)) < 1 ||
budget.value("maxAuditEntries", int64_t(0)) > 4096)
{
return std::nullopt;
}
if (!policy["policyHash"].is_string() || policy["policyHash"].get<std::string>().size() != 64) {
return std::nullopt;
}
json hash_input = policy;
hash_input.erase("policyHash");
if (sha256_hex(hash_input.dump()) != policy["policyHash"].get<std::string>()) return std::nullopt;
return policy;
}
/* Performance and fault limits are a metadata-only receipt. It makes the
* bounded thresholds and recovery invariants observable without allocating
* the large test workloads while opening a project. */
std::optional<json> performance_fault_matrix_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P6-002-PerformanceFaultMatrixReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const json matrix = json::parse(source->value("source", std::string()), nullptr, false);
if (matrix.is_discarded() || !matrix.is_object() || matrix.value("schemaVersion", 0) != 1 ||
matrix.value("receiptId", std::string()) != "workflow:performance_fault_recovery_matrix" ||
!matrix.contains("thresholds") || !matrix.contains("faults") || !matrix.contains("recovery") ||
!matrix.contains("budget") || !matrix.contains("matrixHash") || !matrix["thresholds"].is_array() ||
matrix["thresholds"].size() < 7 || matrix["thresholds"].size() > 32 ||
!matrix["faults"].is_object() || !matrix["recovery"].is_object() || !matrix["budget"].is_object())
{
return std::nullopt;
}
const std::unordered_set<std::string> required_thresholds = {
"geometry-100k", "geometry-1m", "geometry-10m", "texture-4k", "texture-8k", "long-media", "simulation-cache"};
std::unordered_set<std::string> seen_thresholds;
for (const json &threshold : matrix["thresholds"]) {
if (!threshold.is_object() || !threshold.contains("id") || !threshold["id"].is_string() ||
!threshold.contains("limit") || !threshold["limit"].is_number_integer() ||
threshold.value("limit", int64_t(0)) < 1 || threshold.value("status", std::string()) != "PASS" ||
!seen_thresholds.insert(threshold["id"].get<std::string>()).second)
{
return std::nullopt;
}
}
for (const std::string &id : required_thresholds) if (!seen_thresholds.count(id)) return std::nullopt;
const std::array<std::pair<const char *, const char *>, 4> fault_expectations = {{{"oom", "WASM_OUT_OF_MEMORY"},
{"quota", "STORAGE_QUOTA"},
{"deviceLoss", "GPU_DEVICE_LOSS_BLOCKED"},
{"networkInterrupt", "NETWORK_INTERRUPT_BLOCKED"}}};
for (const auto &[name, code] : fault_expectations) {
if (!matrix["faults"].contains(name) || !matrix["faults"][name].is_object() ||
matrix["faults"][name].value("status", std::string()) != "BLOCKED" ||
matrix["faults"][name].value("code", std::string()) != code ||
matrix["faults"][name].value("publishedResults", int64_t(-1)) != 0)
{
return std::nullopt;
}
}
const json &recovery = matrix["recovery"];
if (!recovery.value("oldRevisionUsable", false) || recovery.value("revisionBefore", int64_t(-1)) != 1 ||
recovery.value("revisionAfter", int64_t(-1)) != 1 || recovery.value("resourcesAfter", int64_t(-1)) != 0 ||
recovery.value("cache", std::string()) != "QUARANTINED" ||
recovery.value("workerRestart", std::string()) != "RECOVERED")
{
return std::nullopt;
}
const json &budget = matrix["budget"];
for (const char *field : {"maxGeometryTriangles", "maxTextureBytes", "maxMediaBytes", "maxCacheBytes"}) {
if (!budget.contains(field) || !budget[field].is_number_integer() || budget.value(field, int64_t(0)) < 1)
return std::nullopt;
}
if (!matrix["matrixHash"].is_string() || matrix["matrixHash"].get<std::string>().size() != 64) return std::nullopt;
json hash_input = matrix;
hash_input.erase("matrixHash");
if (sha256_hex(hash_input.dump()) != matrix["matrixHash"].get<std::string>()) return std::nullopt;
return matrix;
}
/* Release evidence is a read-only receipt. Only the bounded identity and
* gate status are exposed; opening a blend never executes CI or release work. */
std::optional<json> release_candidate_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P6-003-OfflineCiSbomRollbackReleaseReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const json receipt = json::parse(source->value("source", std::string()), nullptr, false);
if (receipt.is_discarded() || !receipt.is_object() || receipt.value("schemaVersion", 0) != 1 ||
receipt.value("receiptId", std::string()) != "workflow:offline_ci_sbom_and_rollback_release" ||
!receipt.contains("releaseId") || !receipt["releaseId"].is_string() ||
!receipt.contains("ci") || !receipt["ci"].is_object() || !receipt.contains("gates") ||
!receipt["gates"].is_object() || !receipt.contains("archives") || !receipt["archives"].is_object() ||
!receipt.contains("sbom") || !receipt["sbom"].is_object() || !receipt.contains("rollback") ||
!receipt["rollback"].is_object())
{
return std::nullopt;
}
const auto digest = [](const json &value) {
if (!value.is_string()) return false;
const std::string text = value.get<std::string>();
return text.size() == 64 && std::all_of(text.begin(), text.end(), [](const char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
};
const json &quick = receipt["ci"]["quick"];
if (!quick.is_object() || quick.value("status", std::string()) != "READY" ||
!digest(quick.value("reportSha256", json(nullptr))))
{
return std::nullopt;
}
for (const char *gate : {"offlineReproducibility", "sbom", "rollback"}) {
if (!receipt["gates"].contains(gate) || !receipt["gates"][gate].is_object() ||
receipt["gates"][gate].value("status", std::string()) != "READY")
{
return std::nullopt;
}
}
for (const char *archive : {"binary", "source"}) {
if (!receipt["archives"].contains(archive) || !receipt["archives"][archive].is_object() ||
!digest(receipt["archives"][archive].value("sha256", json(nullptr))) ||
receipt["archives"][archive].value("bytes", int64_t(0)) < 1)
{
return std::nullopt;
}
}
if (!digest(receipt["sbom"].value("sha256", json(nullptr))) ||
!digest(receipt["rollback"].value("runbookSha256", json(nullptr))))
{
return std::nullopt;
}
return receipt;
}
/* The full-gap inventory is evidence metadata only. It deliberately exposes
* no feature parity claim; malformed or ambiguous receipts stay blocked. */
std::optional<json> gap_inventory_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P7-001-ClusterFullBlenderGapInventoryReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const std::string text = source->value("source", std::string());
if (text.empty() || text.size() > 262144) return std::nullopt;
const json receipt = json::parse(text, nullptr, false);
if (receipt.is_discarded() || !receipt.is_object() || receipt.value("schemaVersion", 0) != 1 ||
receipt.value("receiptId", std::string()) != "workflow:cluster_full_blender_gap_inventory" ||
receipt.value("runtime", std::string()) != "BLENDER_5_2_INVENTORY" ||
!receipt.contains("inventorySource") || !receipt["inventorySource"].is_object() ||
!receipt.contains("classificationPolicy") || !receipt["classificationPolicy"].is_object() ||
!receipt.contains("clusters") || !receipt["clusters"].is_array() || receipt["clusters"].size() != 15 ||
!receipt.contains("summary") || !receipt["summary"].is_object() ||
!receipt.contains("riskRanking") || !receipt["riskRanking"].is_array())
{
return std::nullopt;
}
const json &source_info = receipt["inventorySource"];
const std::string source_hash = source_info.value("sha256", std::string());
if (source_info.value("path", std::string()) != "tests/golden/WBV2-P0-002/parity-map.json" ||
source_info.value("entryCount", int64_t(-1)) != 6900 || source_hash.size() != 64 ||
!std::all_of(source_hash.begin(), source_hash.end(), [](const char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
}))
{
return std::nullopt;
}
const json &policy = receipt["classificationPolicy"];
if (policy.value("unprovenClass", std::string()) != "METADATA_ONLY" ||
policy.value("parityLevel", std::string()) != "L0" ||
!policy.value("featureParityRequires", json::array()).is_array() ||
policy["featureParityRequires"].size() != 3 ||
!policy.value("negativeOnlyClasses", json::array()).is_array())
{
return std::nullopt;
}
std::unordered_set<std::string> capability_ids;
int64_t entries = 0;
for (const json &cluster : receipt["clusters"]) {
if (!cluster.is_object() || !cluster.contains("capabilityId") || !cluster["capabilityId"].is_string() ||
!cluster.contains("entryCount") || !cluster["entryCount"].is_number_integer() ||
!cluster.contains("legacyCompleted") || !cluster["legacyCompleted"].is_number_integer() ||
!cluster.contains("legacyPending") || !cluster["legacyPending"].is_number_integer() ||
!cluster.contains("legacyActive") || !cluster["legacyActive"].is_number_integer() ||
cluster.value("entryCount", int64_t(-1)) < 1 || cluster.value("legacyCompleted", int64_t(-1)) < 0 ||
cluster.value("legacyPending", int64_t(-1)) < 0 || cluster.value("legacyActive", int64_t(-1)) < 0 ||
cluster.value("entryCount", int64_t(-1)) != cluster.value("legacyCompleted", int64_t(-1)) +
cluster.value("legacyPending", int64_t(-1)) +
cluster.value("legacyActive", int64_t(-1)) ||
cluster.value("currentParityLevel", std::string()) != "L0" ||
cluster.value("completionClass", std::string()) != "METADATA_ONLY" ||
cluster.value("evidenceStatus", std::string()) != "NOT_RUN" ||
cluster.value("webStatus", std::string()) != "GAP" ||
!cluster.contains("riskScore") || !cluster["riskScore"].is_number_integer() ||
cluster.value("riskScore", int64_t(-1)) < 0 || !cluster.contains("representativeIds") ||
!cluster["representativeIds"].is_array() || cluster["representativeIds"].empty())
{
return std::nullopt;
}
if (!capability_ids.insert(cluster["capabilityId"].get<std::string>()).second) return std::nullopt;
entries += cluster.value("entryCount", int64_t(0));
}
const json &summary = receipt["summary"];
if (summary.value("inventoryEntries", int64_t(-1)) != 6900 || summary.value("clusterCount", int64_t(-1)) != 15 ||
summary.value("unknownEntries", int64_t(-1)) != 0 || summary.value("currentParityClaims", int64_t(-1)) != 0 ||
summary.value("currentParityLevel", std::string()) != "L0" || summary.value("webGapEntries", int64_t(-1)) != 6900 ||
entries != 6900 || receipt["riskRanking"].size() != 15)
{
return std::nullopt;
}
std::unordered_set<std::string> ranking;
for (const json &value : receipt["riskRanking"]) {
if (!value.is_string() || !ranking.insert(value.get<std::string>()).second || !capability_ids.count(value.get<std::string>())) return std::nullopt;
}
if (ranking.size() != capability_ids.size()) return std::nullopt;
return receipt;
}
std::optional<json> atomic_gap_queue_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P7-002-AtomicFamilyGapQueueReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const std::string text = source->value("source", std::string());
if (text.empty() || text.size() > 262144) return std::nullopt;
const json receipt = json::parse(text, nullptr, false);
if (receipt.is_discarded() || !receipt.is_object() || receipt.value("schemaVersion", 0) != 1 ||
receipt.value("receiptId", std::string()) != "workflow:generate_atomic_family_gap_queue" ||
!receipt.contains("parent") || !receipt["parent"].is_object() || !receipt.contains("queue") ||
!receipt["queue"].is_object() || !receipt.contains("tasks") || !receipt["tasks"].is_array() ||
receipt["tasks"].size() != 45 || receipt.value("nextTask", std::string()) != "WBV2-P7-003")
{
return std::nullopt;
}
const json &parent = receipt["parent"];
const std::string manifest_hash = parent.value("manifestSha256", std::string());
const std::string receipt_hash = parent.value("receiptSha256", std::string());
const auto valid_hash = [](const std::string &value) {
return value.size() == 64 && std::all_of(value.begin(), value.end(), [](const char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
};
if (parent.value("task", std::string()) != "WBV2-P7-001" ||
parent.value("manifestPath", std::string()) != "tests/golden/WBV2-P7-001/manifest.json" ||
!valid_hash(manifest_hash) || !valid_hash(receipt_hash) ||
parent.value("receiptPath", std::string()) != "tests/golden/WBV2-P7-001/gap-inventory-desktop-report.json")
{
return std::nullopt;
}
const json &queue = receipt["queue"];
if (queue.value("activeCount", int64_t(-1)) != 1 || queue.value("plannedCount", int64_t(-1)) != 44 ||
queue.value("activeTaskId", std::string()).empty() || !queue.contains("behaviors") ||
!queue["behaviors"].is_array() || queue["behaviors"].size() != 3 ||
queue["behaviors"][0] != "desktopPositive" || queue["behaviors"][1] != "wasmMutation" ||
queue["behaviors"][2] != "saveReopenExact")
{
return std::nullopt;
}
std::unordered_set<std::string> ids;
size_t active = 0;
for (size_t index = 0; index < receipt["tasks"].size(); index++) {
const json &task = receipt["tasks"][index];
const std::string id = task.value("id", std::string());
const std::string behavior = task.value("behavior", std::string());
const bool expected_active = index == 0;
const std::string expected_behavior = index % 3 == 0 ? "desktopPositive" : index % 3 == 1 ? "wasmMutation" : "saveReopenExact";
if (!task.is_object() || id.empty() || !ids.insert(id).second ||
task.value("parityId", std::string()).empty() || task.value("parityId", std::string()) != task.value("capabilityId", std::string()) ||
behavior != expected_behavior || task.value("implementationClass", std::string()) != "LOCAL_BOUNDED" ||
task.value("parentTask", std::string()) != "WBV2-P7-001" || !valid_hash(task.value("parentReceiptSha256", std::string())) ||
task.value("state", std::string()) != (expected_active ? "active" : "pending") || task.value("ready", false) != expected_active)
{
return std::nullopt;
}
active += expected_active ? 1 : 0;
}
if (active != 1 || queue.value("activeTaskId", std::string()) != receipt["tasks"][0].value("id", std::string())) return std::nullopt;
return receipt;
}
std::optional<json> parity_release_gate_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P7-003-FullParityCandidateReleaseGateReceipt") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const std::string text = source->value("source", std::string());
if (text.empty() || text.size() > 131072) return std::nullopt;
const json receipt = json::parse(text, nullptr, false);
if (receipt.is_discarded() || !receipt.is_object() || receipt.value("schemaVersion", 0) != 1 ||
receipt.value("receiptId", std::string()) != "workflow:full_parity_candidate_release_gate" ||
receipt.value("runtime", std::string()) != "BLENDER_DESKTOP_WASM_MAIN" ||
!receipt.contains("inputs") || !receipt["inputs"].is_object() || receipt["inputs"].size() != 4 ||
!receipt.contains("parityClosure") || !receipt["parityClosure"].is_object() ||
!receipt.contains("evidenceHealth") || !receipt["evidenceHealth"].is_object() ||
!receipt.contains("recoveryHealth") || !receipt["recoveryHealth"].is_object() ||
!receipt.contains("releaseDecision") || !receipt["releaseDecision"].is_object())
{
return std::nullopt;
}
const auto valid_hash = [](const json &value) {
if (!value.is_string()) return false;
const std::string text = value.get<std::string>();
return text.size() == 64 && std::all_of(text.begin(), text.end(), [](const char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
};
for (const auto &[name, input] : receipt["inputs"].items()) {
if (name.empty() || !input.is_object() || input.value("path", std::string()).empty() ||
!valid_hash(input.value("sha256", json(nullptr)))) return std::nullopt;
}
const json &closure = receipt["parityClosure"];
if (closure.value("status", std::string()) != "BLOCKED" || closure.value("inventoryEntries", int64_t(-1)) != 6900 ||
closure.value("clusterCount", int64_t(-1)) != 15 || closure.value("atomicTaskCount", int64_t(-1)) != 45 ||
closure.value("currentParityLevel", std::string()) != "L0" || closure.value("featureParityClaims", int64_t(-1)) != 0 ||
!closure.value("reasonCodes", json::array()).is_array() || closure["reasonCodes"].empty()) return std::nullopt;
const json &evidence = receipt["evidenceHealth"];
if (evidence.value("status", std::string()) != "BLOCKED" ||
evidence.value("historicalEvidencePreserved", false) != true ||
evidence.value("m12ProvenanceState", std::string()) != "BLOCKED_SOURCE_LOST" ||
evidence.value("m12Entries", int64_t(0)) < 1 || !evidence.value("reasonCodes", json::array()).is_array() ||
evidence["reasonCodes"].empty()) return std::nullopt;
const json &recovery = receipt["recoveryHealth"];
if (recovery.value("status", std::string()) != "READY" || recovery.value("quickCi", std::string()) != "READY" ||
recovery.value("inventorySaveReopen", std::string()) != "EXACT" || recovery.value("queueSaveReopen", std::string()) != "EXACT" ||
recovery.value("malformedBlend", std::string()) != "REJECTED_WITHOUT_MAIN_MUTATION") return std::nullopt;
const json &decision = receipt["releaseDecision"];
if (decision.value("status", std::string()) != "BLOCKED" || !decision["nextTask"].is_null() ||
!decision.value("reasonCodes", json::array()).is_array() || decision["reasonCodes"].empty()) return std::nullopt;
return receipt;
}
/* Worker lifecycle receipts are persisted as data, while the actual Worker
* remains an application concern. Validate the bounded receipt before it is
* exposed so desktop and WASM agree on cancellation, generation and release
* semantics without executing any user code. */
std::optional<json> worker_lifecycle_contract_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P1-004-WorkerLifecycleContract") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const std::string text = source->value("source", std::string());
if (text.empty() || text.size() > 65536) return std::nullopt;
const json contract = json::parse(text, nullptr, false);
if (contract.is_discarded() || !contract.is_object() ||
contract.value("schemaVersion", 0) != 1 ||
contract.value("contractId", std::string()) !=
"workflow:harden_worker_cancel_restart_and_dispose" ||
!contract.contains("cancel") || !contract.contains("generation") ||
!contract.contains("owners") || !contract.contains("budget") ||
!contract.contains("dispose") || !contract.contains("schemaHash"))
{
return std::nullopt;
}
if (!contract["cancel"].is_object() || !contract["generation"].is_object() ||
!contract["owners"].is_object() || !contract["budget"].is_object() ||
!contract["dispose"].is_object() || !contract["schemaHash"].is_string() ||
contract["schemaHash"].get<std::string>().size() != 64)
{
return std::nullopt;
}
if (contract["cancel"].value("code", std::string()) != "OPEN_CANCELLED" ||
contract["cancel"].value("resultPublished", true) ||
!contract["cancel"].value("preservesRevision", false) ||
contract["generation"].value("initial", int64_t(0)) != 1 ||
contract["generation"].value("afterRestart", int64_t(0)) != 2 ||
contract["generation"].value("staleResult", std::string()) != "REJECTED" ||
!contract["dispose"].value("idempotent", false) ||
contract["dispose"].value("doubleDispose", std::string()) != "NOOP")
{
return std::nullopt;
}
for (const char *owner_name : {"cpu", "wasm", "gpu"}) {
if (!contract["owners"].contains(owner_name) ||
!contract["owners"][owner_name].is_object() ||
contract["owners"][owner_name].value("afterCancel", int64_t(-1)) < 0 ||
contract["owners"][owner_name].value("afterRestart", int64_t(-1)) < 0 ||
contract["owners"][owner_name].value("afterDispose", int64_t(-1)) != 0)
{
return std::nullopt;
}
}
if (contract["budget"].value("maxLiveRequests", int64_t(0)) < 1 ||
contract["budget"].value("maxLiveRequests", int64_t(0)) > 16 ||
contract["budget"].value("maxInputBytes", int64_t(0)) < 1 ||
contract["budget"].value("maxInputBytes", int64_t(0)) > 64 * 1024 * 1024)
{
return std::nullopt;
}
json schema_input = contract;
schema_input.erase("schemaHash");
if (sha256_hex(schema_input.dump()) != contract["schemaHash"].get<std::string>()) {
return std::nullopt;
}
return contract;
}
std::optional<json> storage_journal_contract_from_record(const ParsedBlend &blend,
const ElementRef &element,
const IdRecord &record)
{
if (record.name != "WBV2-P1-005-StorageJournalContract") return std::nullopt;
const std::optional<json> source = script_source_from_record(blend, element, record);
if (!source) return std::nullopt;
const std::string text = source->value("source", std::string());
if (text.empty() || text.size() > 65536) return std::nullopt;
const json contract = json::parse(text, nullptr, false);
if (contract.is_discarded() || !contract.is_object() ||
contract.value("schemaVersion", 0) != 1 ||
contract.value("contractId", std::string()) !=
"workflow:harden_opfs_indexeddb_journal_recovery" ||
!contract.contains("journal") || !contract.contains("hash") ||
!contract.contains("storage") || !contract.contains("restart") ||
!contract.contains("budget") || !contract.contains("schemaHash"))
{
return std::nullopt;
}
if (!contract["journal"].is_object() || !contract["hash"].is_object() ||
!contract["storage"].is_object() || !contract["restart"].is_object() ||
!contract["budget"].is_object() || !contract["schemaHash"].is_string() ||
contract["schemaHash"].get<std::string>().size() != 64)
{
return std::nullopt;
}
if (contract["journal"].value("interruptedAt", std::string()) != "BEFORE_PUBLISH" ||
!contract["journal"].value("oldProjectUsable", false) ||
contract["journal"].value("recovery", std::string()) != "ROLLBACK_OLD_REVISION" ||
contract["hash"].value("mismatch", std::string()) != "QUARANTINED" ||
contract["hash"].value("verified", false) ||
contract["restart"].value("generationBefore", int64_t(0)) != 1 ||
contract["restart"].value("generationAfter", int64_t(0)) != 2 ||
contract["restart"].value("status", std::string()) != "RECOVERED")
{
return std::nullopt;
}
for (const char *store_name : {"opfs", "indexeddb"}) {
if (!contract["storage"].contains(store_name) ||
!contract["storage"][store_name].is_object() ||
contract["storage"][store_name].value("afterRecovery", int64_t(-1)) != 0)
{
return std::nullopt;
}
}
if (contract["budget"].value("maxProjectBytes", int64_t(0)) < 1 ||
contract["budget"].value("maxProjectBytes", int64_t(0)) > 64 * 1024 * 1024 ||
contract["budget"].value("maxJournalEntries", int64_t(0)) < 1 ||
contract["budget"].value("maxJournalEntries", int64_t(0)) > 1024)
{
return std::nullopt;
}
json schema_input = contract;
schema_input.erase("schemaHash");
if (sha256_hex(schema_input.dump()) != contract["schemaHash"].get<std::string>()) {
return std::nullopt;
}
return contract;
}
std::vector<uint64_t> read_raw_pointer_array(const ParsedBlend &blend,
const uint64_t pointer,
const size_t count)
@@ -3453,6 +4137,39 @@ json scene_ir_from_blend(const ParsedBlend &blend,
json libraries = json::array();
json script_sources = json::array();
size_t script_source_bytes = 0;
json capability_contract = json::object();
bool capability_contract_available = false;
bool capability_contract_blocked = false;
json render_routing = json::object();
bool render_routing_available = false;
bool render_routing_blocked = false;
json server_job_binding = json::object();
bool server_job_binding_available = false;
bool server_job_binding_blocked = false;
json scripting_policy = json::object();
bool scripting_policy_available = false;
bool scripting_policy_blocked = false;
json performance_fault_matrix = json::object();
bool performance_fault_matrix_available = false;
bool performance_fault_matrix_blocked = false;
json release_candidate = json::object();
bool release_candidate_available = false;
bool release_candidate_blocked = false;
json gap_inventory = json::object();
bool gap_inventory_available = false;
bool gap_inventory_blocked = false;
json atomic_gap_queue = json::object();
bool atomic_gap_queue_available = false;
bool atomic_gap_queue_blocked = false;
json parity_release_gate = json::object();
bool parity_release_gate_available = false;
bool parity_release_gate_blocked = false;
json worker_lifecycle_contract = json::object();
bool worker_lifecycle_contract_available = false;
bool worker_lifecycle_contract_blocked = false;
json storage_journal_contract = json::object();
bool storage_journal_contract_available = false;
bool storage_journal_contract_blocked = false;
json masks = json::array();
bool masks_blocked = false;
bool libraries_blocked = false;
@@ -4147,6 +4864,182 @@ json scene_ir_from_blend(const ParsedBlend &blend,
non_mesh_data.push_back(non_mesh_data_from_record(blend, element, record, ids_by_pointer));
}
else if (record.type_name == "Text") {
if (record.name == "WBV2-P0-003-CapabilityContract") {
if (capability_contract_available || capability_contract_blocked) {
capability_contract_blocked = true;
capability_contract_available = false;
capability_contract = json::object();
}
else if (const std::optional<json> contract = capability_contract_from_record(
blend, element, record))
{
capability_contract = *contract;
capability_contract_available = true;
}
else {
capability_contract_blocked = true;
}
}
if (record.name == "WBV2-P1-004-WorkerLifecycleContract") {
if (worker_lifecycle_contract_available || worker_lifecycle_contract_blocked) {
worker_lifecycle_contract_blocked = true;
worker_lifecycle_contract_available = false;
worker_lifecycle_contract = json::object();
}
else if (const std::optional<json> contract = worker_lifecycle_contract_from_record(
blend, element, record))
{
worker_lifecycle_contract = *contract;
worker_lifecycle_contract_available = true;
}
else {
worker_lifecycle_contract_blocked = true;
}
}
if (record.name == "WBV2-P1-005-StorageJournalContract") {
if (storage_journal_contract_available || storage_journal_contract_blocked) {
storage_journal_contract_blocked = true;
storage_journal_contract_available = false;
storage_journal_contract = json::object();
}
else if (const std::optional<json> contract = storage_journal_contract_from_record(
blend, element, record))
{
storage_journal_contract = *contract;
storage_journal_contract_available = true;
}
else {
storage_journal_contract_blocked = true;
}
}
if (record.name == "WBV2-P5-001-RenderRoutingReceipt") {
if (render_routing_available || render_routing_blocked) {
render_routing_blocked = true;
render_routing_available = false;
render_routing = json::object();
}
else if (const std::optional<json> receipt = render_routing_from_record(
blend, element, record))
{
render_routing = *receipt;
render_routing_available = true;
}
else {
render_routing_blocked = true;
}
}
if (record.name == "WBV2-P5-003-ServerJobBindingReceipt") {
if (server_job_binding_available || server_job_binding_blocked) {
server_job_binding_blocked = true;
server_job_binding_available = false;
server_job_binding = json::object();
}
else if (const std::optional<json> receipt = server_job_binding_from_record(
blend, element, record))
{
server_job_binding = *receipt;
server_job_binding_available = true;
}
else {
server_job_binding_blocked = true;
}
}
if (record.name == "WBV2-P6-001-ScriptingPolicyReceipt") {
if (scripting_policy_available || scripting_policy_blocked) {
scripting_policy_blocked = true;
scripting_policy_available = false;
scripting_policy = json::object();
}
else if (const std::optional<json> policy = scripting_policy_from_record(
blend, element, record))
{
scripting_policy = *policy;
scripting_policy_available = true;
}
else {
scripting_policy_blocked = true;
}
}
if (record.name == "WBV2-P6-002-PerformanceFaultMatrixReceipt") {
if (performance_fault_matrix_available || performance_fault_matrix_blocked) {
performance_fault_matrix_blocked = true;
performance_fault_matrix_available = false;
performance_fault_matrix = json::object();
}
else if (const std::optional<json> matrix = performance_fault_matrix_from_record(
blend, element, record))
{
performance_fault_matrix = *matrix;
performance_fault_matrix_available = true;
}
else {
performance_fault_matrix_blocked = true;
}
}
if (record.name == "WBV2-P6-003-OfflineCiSbomRollbackReleaseReceipt") {
if (release_candidate_available || release_candidate_blocked) {
release_candidate_blocked = true;
release_candidate_available = false;
release_candidate = json::object();
}
else if (const std::optional<json> receipt = release_candidate_from_record(
blend, element, record))
{
release_candidate = *receipt;
release_candidate_available = true;
}
else {
release_candidate_blocked = true;
}
}
if (record.name == "WBV2-P7-001-ClusterFullBlenderGapInventoryReceipt") {
if (gap_inventory_available || gap_inventory_blocked) {
gap_inventory_blocked = true;
gap_inventory_available = false;
gap_inventory = json::object();
}
else if (const std::optional<json> receipt = gap_inventory_from_record(
blend, element, record))
{
gap_inventory = *receipt;
gap_inventory_available = true;
}
else {
gap_inventory_blocked = true;
}
}
if (record.name == "WBV2-P7-002-AtomicFamilyGapQueueReceipt") {
if (atomic_gap_queue_available || atomic_gap_queue_blocked) {
atomic_gap_queue_blocked = true;
atomic_gap_queue_available = false;
atomic_gap_queue = json::object();
}
else if (const std::optional<json> queue = atomic_gap_queue_from_record(
blend, element, record))
{
atomic_gap_queue = *queue;
atomic_gap_queue_available = true;
}
else {
atomic_gap_queue_blocked = true;
}
}
if (record.name == "WBV2-P7-003-FullParityCandidateReleaseGateReceipt") {
if (parity_release_gate_available || parity_release_gate_blocked) {
parity_release_gate_blocked = true;
parity_release_gate_available = false;
parity_release_gate = json::object();
}
else if (const std::optional<json> gate = parity_release_gate_from_record(
blend, element, record))
{
parity_release_gate = *gate;
parity_release_gate_available = true;
}
else {
parity_release_gate_blocked = true;
}
}
if (const std::optional<json> text = script_source_from_record(blend, element, record)) {
json resource = {{"id", record.id}, {"name", record.name}};
resource["source"] = text->value("source", std::string());
@@ -4577,6 +5470,29 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"trackingMaskStatus", masks_blocked ? "BLOCKED" : "AVAILABLE"},
{"editorWorkflowStatus", editor_workflow ? "AVAILABLE" : "BLOCKED"},
{"scriptSourceStatus", script_sources_blocked ? "BLOCKED" : "AVAILABLE"},
{"capabilityContractStatus", capability_contract_blocked ? "BLOCKED" :
capability_contract_available ? "AVAILABLE" : "MISSING"},
{"renderRoutingStatus", render_routing_blocked ? "BLOCKED" :
render_routing_available ? "AVAILABLE" : "MISSING"},
{"serverJobBindingStatus", server_job_binding_blocked ? "BLOCKED" :
server_job_binding_available ? "AVAILABLE" : "MISSING"},
{"scriptingPolicyStatus", scripting_policy_blocked ? "BLOCKED" :
scripting_policy_available ? "AVAILABLE" : "MISSING"},
{"performanceFaultMatrixStatus", performance_fault_matrix_blocked ? "BLOCKED" :
performance_fault_matrix_available ? "AVAILABLE" : "MISSING"},
{"releaseCandidateStatus", release_candidate_blocked ? "BLOCKED" :
release_candidate_available ? "AVAILABLE" : "MISSING"},
{"gapInventoryStatus", gap_inventory_blocked ? "BLOCKED" :
gap_inventory_available ? "AVAILABLE" : "MISSING"},
{"atomicGapQueueStatus", atomic_gap_queue_blocked ? "BLOCKED" :
atomic_gap_queue_available ? "AVAILABLE" : "MISSING"},
{"parityReleaseGateStatus", parity_release_gate_blocked ? "BLOCKED" :
parity_release_gate_available ? "AVAILABLE" : "MISSING"},
{"workerLifecycleContractStatus", worker_lifecycle_contract_blocked ? "BLOCKED" :
worker_lifecycle_contract_available ? "AVAILABLE" :
"MISSING"},
{"storageJournalContractStatus", storage_journal_contract_blocked ? "BLOCKED" :
storage_journal_contract_available ? "AVAILABLE" : "MISSING"},
{"animations", std::move(animations)},
{"nlaTracks", std::move(nla_tracks)},
{"armatures", std::move(armatures)},
@@ -4597,6 +5513,23 @@ json scene_ir_from_blend(const ParsedBlend &blend,
if (!script_sources_blocked) {
snapshot["scriptSources"] = {{"schemaVersion", 1}, {"sources", std::move(script_sources)}};
}
if (capability_contract_available) snapshot["capabilityContract"] = std::move(capability_contract);
if (render_routing_available) snapshot["renderRouting"] = std::move(render_routing);
if (server_job_binding_available) snapshot["serverJobBinding"] = std::move(server_job_binding);
if (scripting_policy_available) snapshot["scriptingPolicy"] = std::move(scripting_policy);
if (performance_fault_matrix_available) {
snapshot["performanceFaultMatrix"] = std::move(performance_fault_matrix);
}
if (release_candidate_available) snapshot["releaseCandidate"] = std::move(release_candidate);
if (gap_inventory_available) snapshot["gapInventory"] = std::move(gap_inventory);
if (atomic_gap_queue_available) snapshot["atomicGapQueue"] = std::move(atomic_gap_queue);
if (parity_release_gate_available) snapshot["parityReleaseGate"] = std::move(parity_release_gate);
if (worker_lifecycle_contract_available) {
snapshot["workerLifecycleContract"] = std::move(worker_lifecycle_contract);
}
if (storage_journal_contract_available) {
snapshot["storageJournalContract"] = std::move(storage_journal_contract);
}
return snapshot;
}

View File

@@ -81,6 +81,7 @@
#include "BLI_listbase.h"
#include "BLI_color.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
@@ -2738,12 +2739,36 @@ bool web_engine_blend_main_set_parent(WebBlendMainState *state,
}
}
float world[4][4];
copy_m4_m4(world, object->object_to_world().ptr());
const auto calculate_world = [&](const Object *value, auto &&self, float result[4][4]) -> void {
float local[4][4];
BKE_object_to_mat4(value, local);
if (value->parent == nullptr) {
copy_m4_m4(result, local);
return;
}
float parent_world[4][4];
float parent_with_inverse[4][4];
self(value->parent, self, parent_world);
mul_m4_m4m4(parent_with_inverse, parent_world, value->parentinv);
mul_m4_m4m4(result, parent_with_inverse, local);
};
calculate_world(object, calculate_world, world);
object->parent = parent;
object->partype = PAROBJECT;
object->parsubstr[0] = '\0';
unit_m4(object->parentinv);
if (keep_transform) BKE_object_apply_mat4(object, world, true, true);
if (keep_transform) {
if (parent != nullptr) {
BKE_object_apply_mat4(object, world, true, true);
}
else {
float rotation[3][3];
float scale[3];
mat4_to_loc_rot_size(object->loc, rotation, scale, world);
mat3_to_eulO(object->rot, object->rotmode, rotation);
copy_v3_v3(object->scale, scale);
}
}
object->id.recalc |= ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY;
return true;
}