import { describe, expect, expectTypeOf, it } from "vitest"; import { toolcraftGpuPassSurfaceSets, type ToolcraftGpuPassExecution, } from "../rendering/renderer-gpu-contract"; import { TOOLCRAFT_FUNCTIONAL_PERFORMANCE_COVERAGE_POLICY } from "./performance-coverage-policy"; import { parseToolcraftRendererPipeline } from "./performance-renderer-pipeline-parser"; import { getRendererPipelineErrorsForEnvelopeContext } from "./performance-renderer-pipeline-validation"; import type { ToolcraftEnvelopePerformanceConfig } from "./performance-types"; import { createSchema } from "./performance-workload-envelope-test-fixtures"; const gpu = { resources: "storage-buffers", stage: "compute", state: "feedback", surfaces: ["preview"], } as const satisfies ToolcraftGpuPassExecution; function createPass(overrides: Record = {}) { return { id: "effect", inputs: ["source.input"], invalidatedBy: ["source.input"], kind: "vector-build", output: "preview", quality: "full", runsOn: "main", ...overrides, }; } function getPipelineErrors(rendererPipeline: Record): string[] { const config = { rendererPipeline, rendererStrategy: "webgpu", scenarios: [], usesCustomRenderer: true, workloadEnvelope: { dimensions: [] }, } as unknown as ToolcraftEnvelopePerformanceConfig; return getRendererPipelineErrorsForEnvelopeContext( { config, parsedPipeline: parseToolcraftRendererPipeline(rendererPipeline), paths: [], schema: createSchema(), workloadEnvelopeValid: false, }, TOOLCRAFT_FUNCTIONAL_PERFORMANCE_COVERAGE_POLICY, ); } function getErrors(pass: Record): string[] { return getPipelineErrors({ interactionInvalidation: [ { interaction: "control-change", invalidates: [pass.id], targets: ["source.input"], }, ], passes: [pass], }); } describe("renderer pipeline GPU execution validation", () => { it.each(["gpu", "worker-or-gpu"])( "requires execution semantics for a %s candidate", (runsOn) => { expect(getErrors(createPass({ runsOn }))).toEqual([ `rendererPipeline pass "effect" runs on ${runsOn} and must declare gpu execution semantics.`, ]); }, ); it("forbids GPU execution semantics on a main pass", () => { expect(getErrors(createPass({ gpu }))).toEqual([ 'rendererPipeline pass "effect" runs on main and must omit gpu execution semantics.', ]); }); it("returns canonical GPU execution semantics from raw parsing", () => { const parsed = parseToolcraftRendererPipeline({ interactionInvalidation: [], passes: [createPass({ gpu, runsOn: "gpu" })], }); const parsedGpu = parsed.pipeline?.passes[0]?.gpu; expect(parsed.errors).toEqual([]); expect(parsedGpu).toEqual(gpu); expect(parsedGpu).not.toBe(gpu); expect(parsedGpu?.surfaces).toBe(toolcraftGpuPassSurfaceSets[0]); expect(Object.isFrozen(parsedGpu)).toBe(true); expect(parsedGpu).not.toHaveProperty("readsPreviousFrame"); expectTypeOf(parsedGpu).toEqualTypeOf(); }); it("rejects an invalid GPU resource kind at its field", () => { expect( getErrors( createPass({ gpu: { ...gpu, resources: "vertex-buffers" }, runsOn: "gpu", }), ), ).toEqual([ 'rendererPipeline.passes[0].gpu.resources must be one of "uniforms-only", "sampled-textures", "storage-buffers", "storage-textures", or "mixed".', ]); }); it("rejects non-storage resources for compute at the resources field", () => { expect( getErrors( createPass({ gpu: { ...gpu, resources: "uniforms-only" }, runsOn: "gpu", }), ), ).toEqual([ 'rendererPipeline.passes[0].gpu.resources must be one of "storage-buffers", "storage-textures", or "mixed" when stage is "compute".', ]); }); it("protects retained upstream access as a retained-resource cache hit", () => { const retained = createPass({ cacheKey: ["source.input"], id: "source", lifecycle: { cache: "retained-resource", resourceScope: "source" }, }); const effect = createPass({ id: "effect" }); const pipeline = (retainedAccesses: unknown, invalidates = ["effect"]) => ({ interactionInvalidation: [ { interaction: "control-change", invalidates, retainedAccesses, targets: ["source.input"], }, ], passes: [retained, effect], }); expect(getPipelineErrors(pipeline(["source"]))).toEqual([]); expect(getPipelineErrors(pipeline(["missing"]))).toContainEqual( expect.stringMatching(/retainedAccesses unknown pass "missing"/u), ); expect(getPipelineErrors(pipeline(["effect"]))).toContainEqual( expect.stringMatching(/retainedAccesses pass "effect".*retained-resource/u), ); expect(getPipelineErrors(pipeline(["source"], ["source"]))).toContainEqual( expect.stringMatching(/both invalidate and retain-access pass "source"/u), ); }); it("validates preparation-only pass invalidation separately from measured invalidation", () => { const pipeline = (preparationInvalidates: unknown, invalidates = ["effect"]) => ({ interactionInvalidation: [ { interaction: "control-change", invalidates, preparationInvalidates, targets: ["source.input"], }, ], passes: [createPass({ id: "effect" }), createPass({ id: "prepare" })], }); expect(getPipelineErrors(pipeline(["prepare"]))).toEqual([]); expect(getPipelineErrors(pipeline(["missing"]))).toContainEqual( expect.stringMatching(/preparationInvalidates unknown pass "missing"/u), ); expect(getPipelineErrors(pipeline(["effect"]))).toEqual([]); }); it.each([ ["empty", []], ["duplicate", ["preview", "preview"]], ["reversed", ["export", "preview"]], ])("rejects %s GPU surface tuples at their field", (_label, surfaces) => { expect( getErrors(createPass({ gpu: { ...gpu, surfaces }, runsOn: "gpu" })), ).toEqual([ 'rendererPipeline.passes[0].gpu.surfaces must be one of ["preview"], ["export"], or ["preview","export"].', ]); }); });