Advance M8-M11 parity workflows
This commit is contained in:
146
web/tests/e2e/texture-paint-asset.spec.ts
Normal file
146
web/tests/e2e/texture-paint-asset.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("M9-11 atomically publishes packed and UDIM dirty tiles after verified OPFS writes", async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async () => {
|
||||
const { StorageClient } = await import("/src/storage/StorageClient.ts");
|
||||
const projectId = "m9-texture-paint-atomic";
|
||||
const hash = async (bytes: Uint8Array | ArrayBuffer) => {
|
||||
const data = bytes instanceof Uint8Array ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : bytes;
|
||||
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", data)), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
const encode = async (pixels: Uint8Array) => {
|
||||
const canvas = new OffscreenCanvas(2, 2);
|
||||
const context = canvas.getContext("2d")!;
|
||||
context.putImageData(new ImageData(new Uint8ClampedArray(pixels), 2, 2), 0, 0);
|
||||
return (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer();
|
||||
};
|
||||
const decode = async (data: ArrayBuffer) => {
|
||||
const bitmap = await createImageBitmap(new Blob([data], { type: "image/png" }));
|
||||
const canvas = new OffscreenCanvas(2, 2);
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true })!;
|
||||
context.drawImage(bitmap, 0, 0);
|
||||
bitmap.close();
|
||||
return new Uint8Array(context.getImageData(0, 0, 2, 2).data);
|
||||
};
|
||||
const commit = async ({
|
||||
client,
|
||||
textureAssetId,
|
||||
kind,
|
||||
tile,
|
||||
revision,
|
||||
sourcePath,
|
||||
baseAssetSha256,
|
||||
basePixels,
|
||||
offset,
|
||||
dirty,
|
||||
faultAt,
|
||||
}: {
|
||||
client: InstanceType<typeof StorageClient>;
|
||||
textureAssetId: string;
|
||||
kind: "PACKED" | "UDIM";
|
||||
tile: number;
|
||||
revision: number;
|
||||
sourcePath: string;
|
||||
baseAssetSha256: string;
|
||||
basePixels: Uint8Array;
|
||||
offset: number;
|
||||
dirty: number[];
|
||||
faultAt?: "after-asset-write";
|
||||
}) => {
|
||||
const next = basePixels.slice();
|
||||
next.set(dirty, offset);
|
||||
return client.commitTexturePaintTile({
|
||||
schemaVersion: 1,
|
||||
target: {
|
||||
schemaVersion: 1,
|
||||
projectId,
|
||||
imageId: "image:M9Paint",
|
||||
textureAssetId,
|
||||
kind,
|
||||
tile,
|
||||
revision,
|
||||
width: 2,
|
||||
height: 2,
|
||||
mimeType: "image/png",
|
||||
colorSpace: "SRGB",
|
||||
sourcePath,
|
||||
baseAssetSha256,
|
||||
},
|
||||
patch: {
|
||||
schemaVersion: 1,
|
||||
textureAssetId,
|
||||
tile,
|
||||
revision,
|
||||
width: 2,
|
||||
height: 2,
|
||||
format: "RGBA8",
|
||||
colorSpace: "SRGB",
|
||||
baseSha256: await hash(basePixels),
|
||||
resultSha256: await hash(next),
|
||||
byteOffset: offset,
|
||||
bytes: new Uint8Array(dirty),
|
||||
},
|
||||
faultAt,
|
||||
});
|
||||
};
|
||||
|
||||
const opaqueBlack = new Uint8Array(Array.from({ length: 4 }, () => [0, 0, 0, 255]).flat());
|
||||
const client = new StorageClient();
|
||||
const packedBase = await client.putAsset(projectId, await encode(opaqueBlack), "image/png", "textures/packed.png");
|
||||
const packedId = "image:M9Paint:packed";
|
||||
const packedFirstPixels = opaqueBlack.slice();
|
||||
packedFirstPixels.set([255, 0, 0, 255], 0);
|
||||
const packedFirst = await commit({ client, textureAssetId: packedId, kind: "PACKED", tile: 1001, revision: 4, sourcePath: "textures/packed.png", baseAssetSha256: packedBase.sha256, basePixels: opaqueBlack, offset: 0, dirty: [255, 0, 0, 255] });
|
||||
const firstAsset = await client.readAsset(projectId, packedFirst.binding.assetSha256);
|
||||
const firstPixels = await decode(firstAsset.data);
|
||||
|
||||
let injected = "";
|
||||
try {
|
||||
await commit({ client, textureAssetId: packedId, kind: "PACKED", tile: 1001, revision: 5, sourcePath: "textures/packed.png", baseAssetSha256: packedFirst.binding.assetSha256, basePixels: packedFirstPixels, offset: 4, dirty: [0, 255, 0, 255], faultAt: "after-asset-write" });
|
||||
}
|
||||
catch (error) { injected = error instanceof Error ? error.message : String(error); }
|
||||
const afterFailure = await client.readTexturePaintTileBinding({ schemaVersion: 1, projectId, textureAssetId: packedId, tile: 1001 });
|
||||
const packedSecondPixels = packedFirstPixels.slice();
|
||||
packedSecondPixels.set([0, 255, 0, 255], 4);
|
||||
const packedSecond = await commit({ client, textureAssetId: packedId, kind: "PACKED", tile: 1001, revision: 5, sourcePath: "textures/packed.png", baseAssetSha256: packedFirst.binding.assetSha256, basePixels: packedFirstPixels, offset: 4, dirty: [0, 255, 0, 255] });
|
||||
|
||||
const udimBase = await client.putAsset(projectId, await encode(opaqueBlack), "image/png", "textures/paint.1002.png");
|
||||
const udimId = "image:M9Paint:tile:1002";
|
||||
const udim = await commit({ client, textureAssetId: udimId, kind: "UDIM", tile: 1002, revision: 5, sourcePath: "textures/paint.1002.png", baseAssetSha256: udimBase.sha256, basePixels: opaqueBlack, offset: 12, dirty: [0, 0, 255, 255] });
|
||||
client.terminate();
|
||||
|
||||
const restarted = new StorageClient();
|
||||
const reopenedPacked = await restarted.readTexturePaintTileBinding({ schemaVersion: 1, projectId, textureAssetId: packedId, tile: 1001 });
|
||||
const reopenedUdim = await restarted.readTexturePaintTileBinding({ schemaVersion: 1, projectId, textureAssetId: udimId, tile: 1002 });
|
||||
const reopenedAsset = await restarted.readAsset(projectId, reopenedPacked.binding!.assetSha256);
|
||||
const reopenedPixels = await decode(reopenedAsset.data);
|
||||
restarted.terminate();
|
||||
return {
|
||||
packedBase: packedBase.sha256,
|
||||
packedFirst: packedFirst.binding,
|
||||
firstPixels: Array.from(firstPixels),
|
||||
injected,
|
||||
afterFailure: afterFailure.binding,
|
||||
packedSecond: packedSecond.binding,
|
||||
udim: udim.binding,
|
||||
reopenedPacked: reopenedPacked.binding,
|
||||
reopenedUdim: reopenedUdim.binding,
|
||||
reopenedPixels: Array.from(reopenedPixels),
|
||||
expectedPackedPixels: Array.from(packedSecondPixels),
|
||||
};
|
||||
});
|
||||
|
||||
expect(result.packedFirst).toMatchObject({ kind: "PACKED", tile: 1001, generation: 1, pixelSha256: expect.stringMatching(/^[a-f0-9]{64}$/) });
|
||||
expect(result.packedFirst.assetSha256).not.toBe(result.packedBase);
|
||||
expect(result.firstPixels.slice(0, 4)).toEqual([255, 0, 0, 255]);
|
||||
expect(result.injected).toContain("STORAGE_TRANSACTION");
|
||||
expect(result.afterFailure).toEqual(result.packedFirst);
|
||||
expect(result.packedSecond).toMatchObject({ kind: "PACKED", tile: 1001, generation: 2 });
|
||||
expect(result.packedSecond.assetSha256).not.toBe(result.packedFirst.assetSha256);
|
||||
expect(result.udim).toMatchObject({ kind: "UDIM", tile: 1002, generation: 1 });
|
||||
expect(result.reopenedPacked).toEqual(result.packedSecond);
|
||||
expect(result.reopenedUdim).toEqual(result.udim);
|
||||
expect(result.reopenedPixels).toEqual(result.expectedPackedPixels);
|
||||
});
|
||||
Reference in New Issue
Block a user