diff --git a/src/core/luma-mask-controller.ts b/src/core/luma-mask-controller.ts index ec56906..fa34ac5 100644 --- a/src/core/luma-mask-controller.ts +++ b/src/core/luma-mask-controller.ts @@ -14,6 +14,7 @@ const LUMA_VIDEO_UPDATE_INTERVAL = 1 / 30; interface ActiveLumaMask { lumaPlayer: LumaPlayer; maskSprite: pixi.Sprite; + maskTexture: pixi.RenderTexture; tempContainer: pixi.Container; contentClip: Player; lastVideoTime: number; @@ -72,6 +73,10 @@ export class LumaMaskController { this.removeEventListeners(); for (const mask of this.activeLumaMasks) { + const wrapper = mask.contentClip.getLumaWrapper(); + if (wrapper && !wrapper.destroyed) { + wrapper.mask = null; + } mask.tempContainer.destroy({ children: true }); mask.maskSprite.destroy({ texture: true }); } @@ -179,10 +184,8 @@ export class LumaMaskController { tempSprite.filters = [invertFilter]; tempContainer.addChild(tempSprite); - const maskTexture = renderer.generateTexture({ - target: tempContainer, - resolution: LUMA_MASK_RESOLUTION - }); + const maskTexture = pixi.RenderTexture.create({ width, height, resolution: LUMA_MASK_RESOLUTION }); + renderer.render({ container: tempContainer, target: maskTexture, clear: true }); const maskSprite = new pixi.Sprite(maskTexture); @@ -191,7 +194,7 @@ export class LumaMaskController { const lumaWrapper = contentClip.getLumaWrapper(); lumaWrapper.mask = maskSprite; - this.activeLumaMasks.push({ lumaPlayer, maskSprite, tempContainer, contentClip, lastVideoTime: -1 }); + this.activeLumaMasks.push({ lumaPlayer, maskSprite, maskTexture, tempContainer, contentClip, lastVideoTime: -1 }); } private updateLumaMasks(): void { @@ -208,13 +211,8 @@ export class LumaMaskController { if (frameChanged) { mask.lastVideoTime = videoTime; - const oldTexture = mask.maskSprite.texture; - mask.maskSprite.texture = renderer.generateTexture({ - target: mask.tempContainer, - resolution: LUMA_MASK_RESOLUTION - }); - - oldTexture.destroy(true); + // Re-render in place: replacing/destroying this texture each frame nulls the mask filter's cached GPU bind group mid-render. + renderer.render({ container: mask.tempContainer, target: mask.maskTexture, clear: true }); } } } diff --git a/tests/luma-mask-controller.test.ts b/tests/luma-mask-controller.test.ts index 97bceaf..adea35f 100644 --- a/tests/luma-mask-controller.test.ts +++ b/tests/luma-mask-controller.test.ts @@ -41,6 +41,9 @@ jest.mock("pixi.js", () => { destroy: jest.fn() })), Texture: jest.fn(), + RenderTexture: { + create: jest.fn(() => ({ destroy: jest.fn() })) + }, ColorMatrixFilter: jest.fn(() => ({ negative: jest.fn() })) @@ -179,7 +182,8 @@ function createMockCanvas() { return { application: { renderer: { - generateTexture: jest.fn(() => createMockTexture()) + generateTexture: jest.fn(() => createMockTexture()), + render: jest.fn() } } }; @@ -249,7 +253,7 @@ describe("LumaMaskController", () => { events.emit("player:loaded", { player: lumaPlayer, trackIndex: 0, clipIndex: 0 }); expect(controller.getActiveMaskCount()).toBe(1); - expect(canvas.application.renderer.generateTexture).toHaveBeenCalled(); + expect(canvas.application.renderer.render).toHaveBeenCalled(); }); it("does NOT create mask when track has only luma player", () => { @@ -579,13 +583,13 @@ describe("LumaMaskController", () => { controller.initialize(); // Emit PlayerLoaded event to trigger mask creation events.emit("player:loaded", { player: lumaPlayer, trackIndex: 0, clipIndex: 0 }); - const initialCalls = (canvas.application.renderer.generateTexture as jest.Mock).mock.calls.length; + const initialCalls = (canvas.application.renderer.render as jest.Mock).mock.calls.length; // Advance video time by more than 1/30 second videoTime = 0.05; controller.update(); - expect((canvas.application.renderer.generateTexture as jest.Mock).mock.calls.length).toBeGreaterThan(initialCalls); + expect((canvas.application.renderer.render as jest.Mock).mock.calls.length).toBeGreaterThan(initialCalls); }); it("does NOT update when frame has not changed enough", () => { @@ -608,13 +612,13 @@ describe("LumaMaskController", () => { // First update to set lastVideoTime videoTime = 0.05; controller.update(); - const callsAfterFirstUpdate = (canvas.application.renderer.generateTexture as jest.Mock).mock.calls.length; + const callsAfterFirstUpdate = (canvas.application.renderer.render as jest.Mock).mock.calls.length; // Small time change (less than 1/30 second) videoTime = 0.06; controller.update(); - expect((canvas.application.renderer.generateTexture as jest.Mock).mock.calls.length).toBe(callsAfterFirstUpdate); + expect((canvas.application.renderer.render as jest.Mock).mock.calls.length).toBe(callsAfterFirstUpdate); }); });