Skip to content

Commit aba5eae

Browse files
perf(core): every graded clip draws through one WebGL context, not one per clip (#5809)
1 parent 7e00f37 commit aba5eae

2 files changed

Lines changed: 316 additions & 170 deletions

File tree

‎packages/core/src/runtime/colorGrading.test.ts‎

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ function createMockWebGl(
122122
} as unknown as WebGLRenderingContext;
123123
}
124124

125+
// Each clip shows its pixels on a 2D canvas, copied from the one shared WebGL canvas.
126+
function mock2d(): CanvasRenderingContext2D {
127+
return { clearRect: vi.fn(), drawImage: vi.fn() } as unknown as CanvasRenderingContext2D;
128+
}
129+
125130
function makeDrawableVideo(): HTMLVideoElement {
126131
const video = document.createElement("video");
127132
video.id = "hero-video";
@@ -182,6 +187,8 @@ describe("createColorGradingRuntime", () => {
182187
let getContextSpy: ReturnType<typeof vi.spyOn>;
183188
let runtime: RuntimeColorGradingApi | null = null;
184189

190+
const webglContexts = () => getContextSpy.mock.calls.filter(([type]) => type === "webgl").length;
191+
185192
beforeEach(() => {
186193
document.body.innerHTML = "";
187194
lastUniform1f = null;
@@ -192,7 +199,7 @@ describe("createColorGradingRuntime", () => {
192199
getContextSpy = vi
193200
.spyOn(HTMLCanvasElement.prototype, "getContext")
194201
.mockImplementation((type: string) =>
195-
type === "webgl" ? createMockWebGl() : null,
202+
type === "webgl" ? createMockWebGl() : type === "2d" ? mock2d() : null,
196203
) as ReturnType<typeof vi.spyOn>;
197204
});
198205

@@ -355,19 +362,49 @@ describe("createColorGradingRuntime", () => {
355362
const recreated = document.querySelector<HTMLCanvasElement>("[data-hf-color-grading-canvas]");
356363
expect(recreated).not.toBeNull();
357364
expect(recreated).toBe(canvas);
358-
expect(getContextSpy).toHaveBeenCalledTimes(1);
365+
expect(webglContexts()).toBe(1);
359366
expect(video.style.getPropertyValue("opacity")).toBe("0");
360367
expect(video.style.getPropertyPriority("opacity")).toBe("important");
361368
});
362369

370+
it("draws every graded clip through one WebGL context, however often a seek run shows and hides them", () => {
371+
const clips = Array.from({ length: 6 }, (_, i) => {
372+
const video = makeDrawableVideo();
373+
video.id = `clip-${i}`;
374+
video.style.display = "none";
375+
document.body.appendChild(video);
376+
return video;
377+
});
378+
runtime = createColorGradingRuntime();
379+
380+
// Each seek shows two neighbouring clips and hides the rest, as scenes come and go.
381+
for (const at of [0, 2, 4, 1, 3, 5, 0, 4]) {
382+
clips.forEach((video, i) => {
383+
const shown = i === at || i === (at + 1) % clips.length;
384+
video.style.display = shown ? "block" : "none";
385+
runtime?.setSourceVisibility(video, shown);
386+
});
387+
}
388+
389+
expect(webglContexts()).toBe(1);
390+
const shared = getContextSpy.mock.instances[0];
391+
const copies = getContextSpy.mock.results
392+
.filter((_, i) => getContextSpy.mock.calls[i]?.[0] === "2d")
393+
.flatMap(
394+
({ value }) => (value as { drawImage: ReturnType<typeof vi.fn> }).drawImage.mock.calls,
395+
);
396+
expect(copies.length).toBeGreaterThan(0);
397+
expect(copies.every(([source]) => source === shared)).toBe(true);
398+
});
399+
363400
it("defers WebGL setup for hidden attributed media until it becomes visible", () => {
364401
const video = makeDrawableVideo();
365402
video.style.display = "none";
366403
document.body.appendChild(video);
367404

368405
runtime = createColorGradingRuntime();
369406

370-
expect(getContextSpy).not.toHaveBeenCalled();
407+
expect(webglContexts()).toBe(0);
371408
expect(document.querySelector("[data-hf-color-grading-canvas]")).toBeNull();
372409
expect(runtime.getStatus(video)).toEqual({
373410
state: "pending",
@@ -376,7 +413,7 @@ describe("createColorGradingRuntime", () => {
376413

377414
video.style.display = "block";
378415
expect(runtime.setSourceVisibility(video, true)).toBe(true);
379-
expect(getContextSpy).toHaveBeenCalledTimes(1);
416+
expect(webglContexts()).toBe(1);
380417
expect(document.querySelector("[data-hf-color-grading-canvas]")).not.toBeNull();
381418
});
382419

@@ -406,12 +443,12 @@ describe("createColorGradingRuntime", () => {
406443
{ id: "warm", dataUrl: "data:image/png;base64,preview" },
407444
],
408445
});
409-
expect(getContextSpy).toHaveBeenCalledTimes(1);
446+
expect(webglContexts()).toBe(1);
410447
expect(document.querySelector("[data-hf-color-grading-canvas]")).toBeNull();
411448
expect(video.style.opacity).toBe("");
412449

413450
await runtime.renderPreviews("#hero-video", [{ id: "mono", grading: "mono-clean" }]);
414-
expect(getContextSpy).toHaveBeenCalledTimes(1);
451+
expect(webglContexts()).toBe(1);
415452

416453
const retinaBatch = await runtime.renderPreviews(
417454
"#hero-video",
@@ -717,7 +754,7 @@ describe("createColorGradingRuntime", () => {
717754

718755
runtime = createColorGradingRuntime();
719756

720-
expect(getContextSpy).toHaveBeenCalledTimes(1);
757+
expect(webglContexts()).toBe(1);
721758
if (!lastUniform1f) throw new Error("Expected WebGL uniform calls");
722759
expect(lastUniform1f).toHaveBeenCalledWith("u_intensity", 0);
723760
expect(lastUniform1f).toHaveBeenCalledWith("u_kuwahara", 0);
@@ -1037,7 +1074,7 @@ describe("createColorGradingRuntime", () => {
10371074

10381075
runtime = createColorGradingRuntime();
10391076

1040-
expect(getContextSpy).toHaveBeenCalledTimes(1);
1077+
expect(webglContexts()).toBe(1);
10411078
expect(document.querySelector("[data-hf-color-grading-canvas]")).not.toBeNull();
10421079
});
10431080

@@ -1079,7 +1116,7 @@ describe("createColorGradingRuntime", () => {
10791116
expect(runtime.setSourceVisibility(video, false)).toBe(true);
10801117
runtime.refresh();
10811118
expect(canvas.isConnected).toBe(false);
1082-
expect(getContextSpy).toHaveBeenCalledTimes(1);
1119+
expect(webglContexts()).toBe(1);
10831120
expect(video.hasAttribute("data-hf-color-grading-source-hidden")).toBe(false);
10841121
});
10851122

@@ -1329,7 +1366,11 @@ describe("createColorGradingRuntime", () => {
13291366

13301367
it("falls back to the untreated shader output when half-float targets are unavailable", () => {
13311368
getContextSpy.mockImplementation((type: string) =>
1332-
type === "webgl" ? createMockWebGl({ halfFloatSupported: false }) : null,
1369+
type === "webgl"
1370+
? createMockWebGl({ halfFloatSupported: false })
1371+
: type === "2d"
1372+
? mock2d()
1373+
: null,
13331374
);
13341375
const video = makeDrawableVideo();
13351376
video.setAttribute(
@@ -1785,7 +1826,11 @@ describe("createColorGradingRuntime", () => {
17851826

17861827
it("reports media texture upload failures in runtime status", () => {
17871828
getContextSpy.mockImplementation((type: string) =>
1788-
type === "webgl" ? createMockWebGl({ failMediaUpload: true }) : null,
1829+
type === "webgl"
1830+
? createMockWebGl({ failMediaUpload: true })
1831+
: type === "2d"
1832+
? mock2d()
1833+
: null,
17891834
);
17901835
const video = makeDrawableVideo();
17911836
document.body.appendChild(video);
@@ -1800,9 +1845,10 @@ describe("createColorGradingRuntime", () => {
18001845

18011846
it("falls back to the source media on WebGL context loss and redraws after restore", () => {
18021847
const { video, canvas } = startRuntimeWithVideo();
1848+
const shared = getContextSpy.mock.instances[0] as HTMLCanvasElement;
18031849

18041850
const lost = new Event("webglcontextlost", { cancelable: true });
1805-
canvas.dispatchEvent(lost);
1851+
shared.dispatchEvent(lost);
18061852

18071853
expect(lost.defaultPrevented).toBe(true);
18081854
expect(canvas.style.display).toBe("none");
@@ -1813,7 +1859,7 @@ describe("createColorGradingRuntime", () => {
18131859
message: "WebGL context lost",
18141860
});
18151861

1816-
canvas.dispatchEvent(new Event("webglcontextrestored"));
1862+
shared.dispatchEvent(new Event("webglcontextrestored"));
18171863

18181864
expect(runtime?.getStatus("#hero-video").state).toBe("active");
18191865
expect(video.hasAttribute("data-hf-color-grading-source-hidden")).toBe(true);

0 commit comments

Comments
 (0)