@@ -9,6 +9,7 @@ import { ExportProgressUI } from "@core/export/export-progress-ui";
99import { ExportError , exportBitrate } from "@core/export/export-utils" ;
1010import { calculateSizeFromPreset } from "@core/output-settings-manager" ;
1111import { canEncodeAudio , canEncodeVideo , CanvasSink , CanvasSource , Input , Output } from "mediabunny" ;
12+ import { RenderTarget , RenderTexture } from "pixi.js" ;
1213
1314const mockOutputs : Array < { cancel : jest . Mock ; addAudioTrack : jest . Mock ; state : string } > = [ ] ;
1415
@@ -53,7 +54,10 @@ jest.mock("pixi.js", () => ({
5354 ...require ( "./helpers/pixi-mock-filters" ) . pixiFilterStubs ,
5455 Rectangle : jest . fn ( ) . mockImplementation ( ( x : number , y : number , width : number , height : number ) => ( { x, y, width, height } ) ) ,
5556 Texture : jest . fn ( ( options : object ) => ( { ...options , destroy : jest . fn ( ) } ) ) ,
56- TextureSource : { from : jest . fn ( ( ) => ( { update : jest . fn ( ) } ) ) }
57+ TextureSource : { from : jest . fn ( ( ) => ( { update : jest . fn ( ) } ) ) } ,
58+ Matrix : jest . fn ( ) ,
59+ RenderTexture : { create : jest . fn ( ( options : object ) => ( { ...options , destroy : jest . fn ( ) } ) ) } ,
60+ RenderTarget : jest . fn ( ) . mockImplementation ( ( options : object ) => ( { ...options , destroy : jest . fn ( ) } ) )
5761} ) ) ;
5862
5963// The canvas package needs TextEncoder, which jsdom lacks; the export only uses its font-name parser.
@@ -97,6 +101,7 @@ function createHarness() {
97101 } ;
98102 const viewAtExtract : Array < typeof viewport > = [ ] ;
99103 const renderer = {
104+ view : { antialias : true } ,
100105 render : jest . fn ( ) ,
101106 extract : {
102107 pixels : jest . fn ( ( ) => {
@@ -451,10 +456,12 @@ describe("off-screen export", () => {
451456 await exporter . exportBlob ( ) ;
452457
453458 expect ( renderer . extract . pixels ) . toHaveBeenCalledTimes ( 2 ) ;
454- expect ( renderer . extract . pixels ) . toHaveBeenCalledWith ( { target : viewport , frame : { x : 0 , y : 0 , width : 4 , height : 2 } , resolution : 1 } ) ;
459+ expect ( RenderTexture . create ) . toHaveBeenCalledWith ( expect . objectContaining ( { width : 4 , height : 2 , resolution : 1 } ) ) ;
460+ // Every draw goes to the export's own target, never to the screen.
461+ expect ( renderer . render ) . toHaveBeenCalledTimes ( 2 ) ;
462+ expect ( renderer . render . mock . calls . every ( ( [ options ] ) => options . container === viewport && options . target ) ) . toBe ( true ) ;
455463 expect ( viewAtExtract ) . toEqual ( [ before , before ] ) ;
456464 expect ( viewport ) . toEqual ( before ) ;
457- expect ( renderer . render ) . not . toHaveBeenCalled ( ) ;
458465 expect ( edit . setExportMode ) . not . toHaveBeenCalled ( ) ;
459466 expect ( edit . executeEditCommand ) . not . toHaveBeenCalled ( ) ;
460467 exporter . dispose ( ) ;
@@ -641,19 +648,34 @@ describe("editor restart", () => {
641648
642649describe ( "output size" , ( ) => {
643650 it ( "draws and encodes at the size the editor lays clips out in, with a bitrate for that size" , async ( ) => {
644- const { exporter, edit, renderer } = createHarness ( ) ;
651+ const { exporter, edit } = createHarness ( ) ;
645652 // An edit sized by resolution "hd" and aspectRatio "9:16", with no output.size.
646653 edit . size = calculateSizeFromPreset ( "hd" , "9:16" ) ;
647654
648655 await exporter . exportBlob ( ) ;
649656
650- expect ( renderer . extract . pixels ) . toHaveBeenCalledWith ( expect . objectContaining ( { frame : { x : 0 , y : 0 , width : 720 , height : 1280 } } ) ) ;
657+ expect ( RenderTexture . create ) . toHaveBeenCalledWith ( expect . objectContaining ( { width : 720 , height : 1280 , resolution : 1 } ) ) ;
651658 expect ( canEncodeVideo ) . toHaveBeenCalledWith ( "avc" , { width : 720 , height : 1280 , bitrate : 2_222_222 } ) ;
652659 expect ( CanvasSource ) . toHaveBeenCalledWith ( expect . objectContaining ( { width : 720 , height : 1280 } ) , { codec : "avc" , bitrate : 2_222_222 } ) ;
653660 exporter . dispose ( ) ;
654661 } ) ;
655662} ) ;
656663
664+ it ( "draws every frame into one render target that has a stencil buffer from its first frame" , async ( ) => {
665+ const { exporter, renderer } = createHarness ( ) ; // 1 s at 2 fps: 2 frames
666+ jest . mocked ( RenderTarget ) . mockClear ( ) ;
667+
668+ await exporter . exportBlob ( ) ;
669+
670+ expect ( RenderTarget ) . toHaveBeenCalledTimes ( 1 ) ;
671+ expect ( RenderTarget ) . toHaveBeenCalledWith ( expect . objectContaining ( { stencil : true } ) ) ;
672+ const target = jest . mocked ( RenderTarget ) . mock . results [ 0 ] . value ;
673+ expect ( renderer . render ) . toHaveBeenCalledTimes ( 2 ) ;
674+ expect ( renderer . render . mock . calls . map ( ( [ options ] ) => options . target ) ) . toEqual ( [ target , target ] ) ;
675+ expect ( target . destroy ) . toHaveBeenCalled ( ) ;
676+ exporter . dispose ( ) ;
677+ } ) ;
678+
657679describe ( "exportBitrate" , ( ) => {
658680 it ( "keeps 5 Mbps at 1080p in either orientation and scales with pixel count" , ( ) => {
659681 expect ( exportBitrate ( { width : 1920 , height : 1080 } ) ) . toBe ( 5_000_000 ) ;
0 commit comments