Files
workinf_Blender_Wasm/web/protocol/render-compositor-media-recovery.ts
mes123456 5a11045ca5
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
Advance Blender 5.2 web parity through M12-03D
2026-08-17 17:30:27 -04:00

198 lines
8.5 KiB
TypeScript

export const RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION = 1 as const;
export const RENDER_COMPOSITOR_MEDIA_DOMAINS = ["RENDER", "COMPOSITOR", "MEDIA"] as const;
export type RenderCompositorMediaDomain = typeof RENDER_COMPOSITOR_MEDIA_DOMAINS[number];
const SHA256 = /^[a-f0-9]{64}$/;
const CANCELLATION_CODES = {
RENDER: "OPEN_CANCELLED",
COMPOSITOR: "COMPOSITOR_CANCELLED",
MEDIA: "SEQUENCER_CANCELLED",
} as const;
const BUDGET_CODES = {
RENDER: "GPU_TEXTURE_BUDGET_EXCEEDED",
COMPOSITOR: "COMPOSITOR_BUDGET_EXCEEDED",
MEDIA: "SEQUENCER_BUDGET_EXCEEDED",
} as const;
export interface RenderCompositorMediaRecoveryEvidenceIR {
schemaVersion: typeof RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION;
domain: RenderCompositorMediaDomain;
source: {
byteLength: number;
sha256: string;
};
cancellation: {
status: "CANCELLED";
code: typeof CANCELLATION_CODES[RenderCompositorMediaDomain];
publishedResults: 0;
temporaryResourcesAfter: 0;
};
restart: {
status: "RECOVERED";
generationBefore: 1;
generationAfter: 2;
identityBefore: string;
identityAfter: string;
outputSha256Before: string;
outputSha256After: string;
};
budget: {
status: "BLOCKED";
code: typeof BUDGET_CODES[RenderCompositorMediaDomain];
retainedIdentityHash: string;
temporaryResourcesAfter: 0;
};
release: {
status: "RELEASED";
releasedBytes: number;
releasedResources: number;
resourcesAfter: 0;
};
recovery: {
status: "RECOVERED";
identityHash: string;
outputSha256: string;
outputBytes: number;
visibleUnits: number;
};
}
function record(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error(`M11_RECOVERY_INVALID: ${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exact(value: Record<string, unknown>, fields: readonly string[], label: string): void {
const allowed = new Set(fields);
const extra = Object.keys(value).find((field) => !allowed.has(field));
if (extra) throw new Error(`M11_RECOVERY_INVALID: ${label}.${extra} is undeclared`);
}
function integer(value: unknown, label: string, minimum = 0): number {
if (!Number.isSafeInteger(value) || (value as number) < minimum) {
throw new Error(`M11_RECOVERY_INVALID: ${label} must be a safe integer >= ${minimum}`);
}
return value as number;
}
function digest(value: unknown, label: string): string {
if (typeof value !== "string" || !SHA256.test(value)) {
throw new Error(`M11_RECOVERY_INVALID: ${label} must be a SHA-256 digest`);
}
return value;
}
function zero(value: unknown, label: string): 0 {
if (value !== 0) throw new Error(`M11_RECOVERY_INVALID: ${label} must be zero`);
return 0;
}
export function parseRenderCompositorMediaRecoveryEvidence(value: unknown): RenderCompositorMediaRecoveryEvidenceIR {
const input = record(value, "evidence");
exact(input, ["schemaVersion", "domain", "source", "cancellation", "restart", "budget", "release", "recovery"], "evidence");
if (input.schemaVersion !== RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION) {
throw new Error("M11_RECOVERY_INVALID: unsupported schema version");
}
if (!RENDER_COMPOSITOR_MEDIA_DOMAINS.includes(input.domain as RenderCompositorMediaDomain)) {
throw new Error("M11_RECOVERY_INVALID: domain is invalid");
}
const domain = input.domain as RenderCompositorMediaDomain;
const source = record(input.source, "source");
exact(source, ["byteLength", "sha256"], "source");
const sourceByteLength = integer(source.byteLength, "source.byteLength", 1);
const sourceSha256 = digest(source.sha256, "source.sha256");
const cancellation = record(input.cancellation, "cancellation");
exact(cancellation, ["status", "code", "publishedResults", "temporaryResourcesAfter"], "cancellation");
if (cancellation.status !== "CANCELLED" || cancellation.code !== CANCELLATION_CODES[domain]) {
throw new Error(`M11_RECOVERY_INVALID: ${domain} cancellation contract is invalid`);
}
zero(cancellation.publishedResults, "cancellation.publishedResults");
zero(cancellation.temporaryResourcesAfter, "cancellation.temporaryResourcesAfter");
const restart = record(input.restart, "restart");
exact(restart, ["status", "generationBefore", "generationAfter", "identityBefore", "identityAfter", "outputSha256Before", "outputSha256After"], "restart");
if (restart.status !== "RECOVERED" || restart.generationBefore !== 1 || restart.generationAfter !== 2) {
throw new Error("M11_RECOVERY_INVALID: restart must advance exactly from generation 1 to 2");
}
const identityBefore = digest(restart.identityBefore, "restart.identityBefore");
const identityAfter = digest(restart.identityAfter, "restart.identityAfter");
const outputSha256Before = digest(restart.outputSha256Before, "restart.outputSha256Before");
const outputSha256After = digest(restart.outputSha256After, "restart.outputSha256After");
if (identityBefore !== identityAfter || outputSha256Before !== outputSha256After) {
throw new Error("M11_RECOVERY_INVALID: restart changed committed identity or output");
}
const budget = record(input.budget, "budget");
exact(budget, ["status", "code", "retainedIdentityHash", "temporaryResourcesAfter"], "budget");
if (budget.status !== "BLOCKED" || budget.code !== BUDGET_CODES[domain]) {
throw new Error(`M11_RECOVERY_INVALID: ${domain} budget contract is invalid`);
}
const retainedIdentityHash = digest(budget.retainedIdentityHash, "budget.retainedIdentityHash");
if (retainedIdentityHash !== identityAfter) {
throw new Error("M11_RECOVERY_INVALID: budget failure changed committed identity");
}
zero(budget.temporaryResourcesAfter, "budget.temporaryResourcesAfter");
const release = record(input.release, "release");
exact(release, ["status", "releasedBytes", "releasedResources", "resourcesAfter"], "release");
if (release.status !== "RELEASED") throw new Error("M11_RECOVERY_INVALID: release.status");
const releasedBytes = integer(release.releasedBytes, "release.releasedBytes", 1);
const releasedResources = integer(release.releasedResources, "release.releasedResources", 1);
zero(release.resourcesAfter, "release.resourcesAfter");
const recovery = record(input.recovery, "recovery");
exact(recovery, ["status", "identityHash", "outputSha256", "outputBytes", "visibleUnits"], "recovery");
if (recovery.status !== "RECOVERED") throw new Error("M11_RECOVERY_INVALID: recovery.status");
const recoveryIdentity = digest(recovery.identityHash, "recovery.identityHash");
const recoveryOutput = digest(recovery.outputSha256, "recovery.outputSha256");
if (recoveryIdentity !== identityAfter || recoveryOutput !== outputSha256After) {
throw new Error("M11_RECOVERY_INVALID: recovered identity or output does not match generation 2");
}
const outputBytes = integer(recovery.outputBytes, "recovery.outputBytes", 1);
const visibleUnits = integer(recovery.visibleUnits, "recovery.visibleUnits", 1);
return {
schemaVersion: RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION,
domain,
source: { byteLength: sourceByteLength, sha256: sourceSha256 },
cancellation: {
status: "CANCELLED",
code: CANCELLATION_CODES[domain],
publishedResults: 0,
temporaryResourcesAfter: 0,
},
restart: {
status: "RECOVERED",
generationBefore: 1,
generationAfter: 2,
identityBefore,
identityAfter,
outputSha256Before,
outputSha256After,
},
budget: {
status: "BLOCKED",
code: BUDGET_CODES[domain],
retainedIdentityHash,
temporaryResourcesAfter: 0,
},
release: { status: "RELEASED", releasedBytes, releasedResources, resourcesAfter: 0 },
recovery: { status: "RECOVERED", identityHash: recoveryIdentity, outputSha256: recoveryOutput, outputBytes, visibleUnits },
};
}
export function parseRenderCompositorMediaRecoverySuite(value: unknown): RenderCompositorMediaRecoveryEvidenceIR[] {
if (!Array.isArray(value) || value.length !== RENDER_COMPOSITOR_MEDIA_DOMAINS.length) {
throw new Error("M11_RECOVERY_INVALID: suite must contain exactly three domains");
}
const reports = value.map(parseRenderCompositorMediaRecoveryEvidence);
if (new Set(reports.map((report) => report.domain)).size !== RENDER_COMPOSITOR_MEDIA_DOMAINS.length) {
throw new Error("M11_RECOVERY_INVALID: suite contains duplicate domains");
}
return RENDER_COMPOSITOR_MEDIA_DOMAINS.map((domain) => reports.find((report) => report.domain === domain)!);
}