From 0a90c9f460b1bea010dd20291cec4841aa1070e3 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 10 Aug 2026 12:16:28 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20capture=5Fpie=5Fmovement=5Fclip?= =?UTF-8?q?=20include=5Fui=20=E2=80=94=20composited=20backbuffer=20(game?= =?UTF-8?q?=20+=20UMG)=20frames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Private/MonolithEditorActions.cpp | 4 ++++ .../Private/MonolithPieSmokeSession.cpp | 23 +++++++++++++++++++ .../Private/MonolithPieSmokeSession.h | 7 ++++++ 3 files changed, 34 insertions(+) diff --git a/Source/MonolithEditor/Private/MonolithEditorActions.cpp b/Source/MonolithEditor/Private/MonolithEditorActions.cpp index f746c6079..3783c1da0 100644 --- a/Source/MonolithEditor/Private/MonolithEditorActions.cpp +++ b/Source/MonolithEditor/Private/MonolithEditorActions.cpp @@ -610,6 +610,7 @@ void FMonolithEditorActions::RegisterActions(FMonolithLogCapture* LogCapture) .Optional(TEXT("marker"), TEXT("string"), TEXT("Log marker token. Default MONOLITH_CLIP."), TEXT("MONOLITH_CLIP")) .Optional(TEXT("duration"), TEXT("number"), TEXT("Seconds the editor loop advances PIE before the session auto-completes (clamped 0-120). Default 5."), TEXT("5")) .Optional(TEXT("capture_interval"), TEXT("number"), TEXT("Seconds between captured frames (clamped 0.05-5). Default 0.25."), TEXT("0.25")) + .Optional(TEXT("include_ui"), TEXT("boolean"), TEXT("Capture the composited backbuffer (game + UMG/Slate UI) via the engine screenshot path — the `shot showui` mechanism — instead of the scene-only viewport read. Frames land at end-of-frame (async), so per-frame uniformity validation is skipped. With PIE docked in the editor the backbuffer includes editor chrome; use new-window PIE for a clean game+UI clip. Default false."), TEXT("false")) .Optional(TEXT("sample_vars"), TEXT("array"), TEXT("AnimInstance variable names sampled each frame. Default [GroundSpeed, bShouldMove, DesiredYawDelta].")) .Optional(TEXT("pawn_class"), TEXT("string"), TEXT("Substring of the target pawn's class name to sample. Omit to use the first player controller's pawn.")) .Optional(TEXT("console_script"), TEXT("array"), TEXT("Console commands run on the PIE world at start (drive the movement).")) @@ -6264,6 +6265,8 @@ FMonolithActionResult FMonolithEditorActions::HandleCapturePieMovementClip(const Session.bCaptureFrames = true; Session.CaptureInterval = Interval; Session.OutputDir = OutputDir; + Params->TryGetBoolField(TEXT("include_ui"), Session.bIncludeUi); + const bool bIncludeUi = Session.bIncludeUi; const FString SessionId = FPieSmokeSessionManager::Get().CreateSession(MoveTemp(Session)); @@ -6276,6 +6279,7 @@ FMonolithActionResult FMonolithEditorActions::HandleCapturePieMovementClip(const Result->SetStringField(TEXT("resolved_output_dir"), OutputDir); // explicit alias for callers Result->SetNumberField(TEXT("duration"), Duration); Result->SetNumberField(TEXT("capture_interval"), Interval); + Result->SetBoolField(TEXT("include_ui"), bIncludeUi); return FMonolithActionResult::Success(Result); } diff --git a/Source/MonolithEditor/Private/MonolithPieSmokeSession.cpp b/Source/MonolithEditor/Private/MonolithPieSmokeSession.cpp index 57fbed6d3..425604f6c 100644 --- a/Source/MonolithEditor/Private/MonolithPieSmokeSession.cpp +++ b/Source/MonolithEditor/Private/MonolithPieSmokeSession.cpp @@ -1290,6 +1290,28 @@ void FPieSmokeSessionManager::AdvanceSession(FPieSmokeSession& Session) const FString FramePath = Session.OutputDir / FString::Printf(TEXT("frame_%03d.png"), Session.CaptureFrameIndex); + + // include_ui: ride the engine screenshot path (the `shot showui` + // mechanism) — Slate composites the full backbuffer (game + UMG) + // and writes the file at END OF FRAME. Asynchronous by nature: + // bSaved/uniformity can't be verified here, so these frames count + // as requested-valid and the clip directory holds the truth. + if (Session.bIncludeUi) + { + IFileManager::Get().MakeDirectory(*Session.OutputDir, true); + FScreenshotRequest::RequestScreenshot(FramePath, /*bShowUI*/true, /*bAddFilenameSuffix*/false); + Sample.FramePath = FramePath; + Sample.bFrameUniform = false; + Sample.bFrameValid = true; + if (Session.CaptureFrameIndex >= Session.DiscardFirstFrames) + { + ++Session.ValidFrames; + } + Session.LastCaptureSeconds = SampleTime; + ++Session.CaptureFrameIndex; + } + else + { // #7 flush the render thread before the very first ReadPixels so a warm-up / // uniform first frame is not produced in the first place. const bool bFirstCapture = (Session.CaptureFrameIndex == 0); @@ -1320,6 +1342,7 @@ void FPieSmokeSessionManager::AdvanceSession(FPieSmokeSession& Session) TEXT("capture_pie_movement_clip: PIE viewport unavailable for session %s — capture deferred."), *Session.Id); } + } // !bIncludeUi (scene-only ReadPixels path) } } diff --git a/Source/MonolithEditor/Private/MonolithPieSmokeSession.h b/Source/MonolithEditor/Private/MonolithPieSmokeSession.h index ae81b958e..b722a3df0 100644 --- a/Source/MonolithEditor/Private/MonolithPieSmokeSession.h +++ b/Source/MonolithEditor/Private/MonolithPieSmokeSession.h @@ -297,6 +297,13 @@ struct FPieSmokeSession FString OutputDir; int32 CaptureFrameIndex = 0; bool bCaptureDeferred = false; // set if viewport capture is unavailable in this build path + /** Capture the composited backbuffer (game + UMG/Slate UI) via the engine + * screenshot path — the `shot showui` mechanism — instead of the raw + * scene-only viewport read. Async (file lands at end of frame), so + * uniform-frame validity checks don't apply to these frames. NOTE: with + * PIE docked in the editor the backbuffer is the whole editor window; + * use new-window PIE for a clean game-plus-UI clip. */ + bool bIncludeUi = false; // #7 first-frame warm-up policy. The first DiscardFirstFrames captured frames are saved to // disk (so the clip is complete) but are NOT counted toward ValidFrames / InvalidFrames —