Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/i
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/install/lib/pkgconfig/
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/install/lib/libhdf5.settings
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/install/include/H5pubconf.h
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/install/share/

# HDF5 third-party (scoped to vendored path)
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/**/java/.classes
Expand All @@ -190,6 +191,7 @@ UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/.
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/CLAUDE.md
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/CMakeUserPresets.json
UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/ThirdParty/hdf5-2.0.0/HDF5Examples/CMakeUserPresets.json
UnrealFolder/ProjectMobius/UnitTestSampleData/**/MobiusCaptures/


# =====================================================
Expand Down
3 changes: 2 additions & 1 deletion UnrealFolder/ProjectMobius/Config/DefaultEngine.ini
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ r.TemporalAA.Upsampling=False
r.DefaultFeature.Bloom=False
r.DefaultBackBufferPixelFormat=4
r.AllowStaticLighting=False
r.MegaLights.EnableForProject=True
r.RayTracing.Shadows=True
r.VertexFoggingForOpaque=False
r.Velocity.EnableVertexDeformation=1
Expand Down Expand Up @@ -374,3 +373,5 @@ AutoRepair=False
bAutoRepair=False
EnabledByDefault=False

[/Script/MacTargetPlatform.XcodeProjectSettings]
bMacSignToRunLocally=True
2 changes: 2 additions & 0 deletions UnrealFolder/ProjectMobius/Config/Mac/MacEngine.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[/Script/Engine.RendererSettings]
r.MegaLights.EnableForProject=False
2 changes: 2 additions & 0 deletions UnrealFolder/ProjectMobius/Config/Windows/WindowsEngine.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[/Script/Engine.RendererSettings]
r.MegaLights.EnableForProject=True
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Fill out your copyright notice in the Description page of Project Settings.


Expand All @@ -7,6 +7,7 @@
#include "HAL/PlatformFileManager.h"
#include "ImageUtils.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "RenderingThread.h" // For FlushRenderingCommands (Mac GPU sync)
#include "Slate/SceneViewport.h"
#include "Subsystems/MobiusCustomLoggerSubsystem.h"
Expand Down Expand Up @@ -76,6 +77,45 @@
});
}
}

#if PLATFORM_MAC
bool IsLikelyMacPrivacyProtectedPath(const FString& Path)
{
FString NormalizedPath = Path;
FPaths::NormalizeFilename(NormalizedPath);

FString UserDir = FPlatformProcess::UserDir();
FPaths::NormalizeDirectoryName(UserDir);

const TArray<FString> ProtectedRoots =
{
FPaths::Combine(UserDir, TEXT("Desktop")),
FPaths::Combine(UserDir, TEXT("Documents")),
FPaths::Combine(UserDir, TEXT("Downloads"))
};

for (FString ProtectedRoot : ProtectedRoots)
{
FPaths::NormalizeDirectoryName(ProtectedRoot);
if (NormalizedPath.StartsWith(ProtectedRoot))
{
return true;
}
}

return false;
}

FString BuildMacWriteFailureDetail(const FString& Path)
{
if (IsLikelyMacPrivacyProtectedPath(Path))
{
return TEXT(" macOS Files & Folders privacy may be blocking access to this Desktop/Documents/Downloads location.");
}

return TEXT(" The path is valid, so the failure is likely a macOS permission or Unreal screenshot write-path issue.");
}
#endif
}

void UFrameGrabberHelper::Configure(bool bInUseFullResolution, FIntPoint InDownscaleSize)
Expand Down Expand Up @@ -103,6 +143,21 @@
#endif
}

void UFrameGrabberHelper::BeginDestroy()
{
#if PLATFORM_MAC
ResetMacCaptureState();

if (bMacScreenshotDelegateBound)
{
UGameViewportClient::OnScreenshotCaptured().RemoveAll(this);
bMacScreenshotDelegateBound = false;
}
#endif

Super::BeginDestroy();
}

#if !PLATFORM_MAC
bool UFrameGrabberHelper::TryInitialize()
{
Expand Down Expand Up @@ -258,35 +313,68 @@
MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Created directory: %s"), *DestPath));
}

// Full path for the screenshot (without .png - engine may add it)
FString FullPath = DestPath / PendingFileName;
PendingOutputPath = DestPath / (PendingFileName + TEXT(".png"));
FPaths::NormalizeFilename(PendingOutputPath);

FString PreflightFailureReason;
if (!PreflightMacScreenshotWrite(DestPath, PreflightFailureReason))
{
MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Error: Write preflight failed for %s. Reason: %s"),
*DestPath,
*PreflightFailureReason));
MobiusReportError(
FText::FromString(TEXT("Save Permission Error")),
FText::FromString(PreflightFailureReason),
FText::FromString(TEXT("FrameGrabberHelper::TriggerCapture")),
EMobiusErrorSeverity::Error,
true);
PendingOutputPath.Empty();
return;
}

if (!bMacScreenshotDelegateBound)
{
UGameViewportClient::OnScreenshotCaptured().AddUObject(this, &UFrameGrabberHelper::HandleMacScreenshotCaptured);
bMacScreenshotDelegateBound = true;
}

MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Requesting screenshot via FScreenshotRequest: %s"), *FullPath));
MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Requesting screenshot via FScreenshotRequest: %s"), *PendingOutputPath));

// Request screenshot with filename, no HDR, show UI notification, no unique suffix
// Signature: RequestScreenshot(const FString& Filename, bool bShowUI, bool bAddFilenameSuffix, bool bInHDR)
FScreenshotRequest::RequestScreenshot(FullPath, true, false, false);
// Request screenshot with filename, no HDR, show UI notification, no unique suffix.
// We still request Unreal's screenshot flow, but the pixel buffer is saved by our own delegate so the
// final write reports explicit permission/path failures instead of silently disappearing.
FScreenshotRequest::RequestScreenshot(PendingOutputPath, true, false, false);

bIsCapturing = true;

// Reset capturing flag after a delay since the engine saves asynchronously
FTimerHandle TimerHandle;
if (GEngine && GEngine->GameViewport && GEngine->GameViewport->GetWorld())
{
GEngine->GameViewport->GetWorld()->GetTimerManager().SetTimer(
TimerHandle,
MacCaptureTimeoutHandle,
[this]() {
bIsCapturing = false;
MobiusLog(TEXT("[FrameGrabber][Mac] Capture flag reset after timeout."));
if (!bIsCapturing)
{
return;
}

const FString TimedOutOutputPath = PendingOutputPath;
ResetMacCaptureState();
MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Error: Screenshot capture timed out before a save callback for %s"), *TimedOutOutputPath));
MobiusReportError(
FText::FromString(TEXT("Capture Timed Out")),
FText::FromString(TEXT("The screenshot was requested, but macOS never delivered image data back to the game. This points to an Unreal capture-path problem rather than simple folder creation.")),
FText::FromString(TEXT("FrameGrabberHelper::TriggerCapture")),
EMobiusErrorSeverity::Error,
true);
},
0.5f,
3.0f,
false
);
}
else
{
MobiusLog(TEXT("[FrameGrabber][Mac] Warning: Could not set timer for capture reset - no valid world context."));
bIsCapturing = false;
ResetMacCaptureState();
}
#else
// Windows/other: Use FFrameGrabber
Expand Down Expand Up @@ -340,6 +428,110 @@
#endif
}

#if PLATFORM_MAC
bool UFrameGrabberHelper::PreflightMacScreenshotWrite(const FString& DestPath, FString& OutFailureReason) const
{
const FString ProbePath = DestPath / FString::Printf(TEXT(".mobius_write_probe_%llu.tmp"), static_cast<uint64>(FPlatformTime::Cycles64()));
const bool bProbeWritten = FFileHelper::SaveStringToFile(
TEXT("probe"),
*ProbePath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);

if (!bProbeWritten)
{
OutFailureReason = FString::Printf(
TEXT("Project Mobius could create or resolve the folder but could not create a test file in %s.%s"),
*DestPath,
*BuildMacWriteFailureDetail(DestPath));
return false;
}

IFileManager::Get().Delete(*ProbePath, false, true, true);
return true;
}

void UFrameGrabberHelper::HandleMacScreenshotCaptured(int32 InSizeX, int32 InSizeY, const TArray<FColor>& InImageData)
{
if (!bIsCapturing)
{
return;
}

if (PendingOutputPath.IsEmpty())
{
MobiusLog(TEXT("[FrameGrabber][Mac] Warning: Received screenshot pixels with no pending output path."));
ResetMacCaptureState();
return;
}

if (InSizeX <= 0 || InSizeY <= 0 || InImageData.Num() == 0)
{
const FString FailedOutputPath = PendingOutputPath;
ResetMacCaptureState();
MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Error: Invalid screenshot pixel data for %s"), *FailedOutputPath));
MobiusReportError(
FText::FromString(TEXT("Capture Failed")),
FText::FromString(TEXT("The screenshot completed but returned no pixel data.")),
FText::FromString(TEXT("FrameGrabberHelper::HandleMacScreenshotCaptured")),
EMobiusErrorSeverity::Error,
true);
return;
}

const FString OutputPath = PendingOutputPath;
TArray<FColor> ImageData = InImageData;
for (FColor& Pixel : ImageData)
{
Pixel.A = 255;
}

Async(EAsyncExecution::ThreadPool, [WeakThis = TWeakObjectPtr<UFrameGrabberHelper>(this), OutputPath, InSizeX, InSizeY, ImageData = MoveTemp(ImageData)]() mutable
{
TArray64<uint8> PNGData;
FImageUtils::PNGCompressImageArray(InSizeX, InSizeY, ImageData, PNGData);

const bool bSaved = PNGData.Num() > 0 && FFileHelper::SaveArrayToFile(PNGData, *OutputPath);

AsyncTask(ENamedThreads::GameThread, [WeakThis, OutputPath, InSizeX, InSizeY, bSaved]()
{
if (!WeakThis.IsValid())
{
return;
}

WeakThis->ResetMacCaptureState();

if (!bSaved)
{
MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Error: Failed to save screenshot after capture: %s"), *OutputPath));
MobiusReportError(
FText::FromString(TEXT("Save Failed")),
FText::FromString(FString::Printf(TEXT("Captured screenshot pixels successfully but could not write %s.%s"), *OutputPath, *BuildMacWriteFailureDetail(OutputPath))),
FText::FromString(TEXT("FrameGrabberHelper::HandleMacScreenshotCaptured")),
EMobiusErrorSeverity::Error,
true);
return;
}

MobiusLog(FString::Printf(TEXT("[FrameGrabber][Mac] Screenshot saved successfully (%dx%d): %s"),
InSizeX, InSizeY, *OutputPath));
});
});
}

void UFrameGrabberHelper::ResetMacCaptureState()
{
if (GEngine && GEngine->GameViewport && GEngine->GameViewport->GetWorld() && MacCaptureTimeoutHandle.IsValid())
{
GEngine->GameViewport->GetWorld()->GetTimerManager().ClearTimer(MacCaptureTimeoutHandle);
}

MacCaptureTimeoutHandle.Invalidate();
PendingOutputPath.Empty();
bIsCapturing = false;
}
#endif

void UFrameGrabberHelper::Tick(float DeltaTime)
{
#if PLATFORM_MAC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class MOBIUSCORE_API UFrameGrabberHelper : public UObject
/** Tick to be called each frame to poll frames internally */
void Tick(float DeltaTime);

virtual void BeginDestroy() override;

bool IsCapturing() const { return bIsCapturing; }
#if PLATFORM_MAC
bool IsInitialized() const { return true; } // Mac uses FScreenshotRequest, always "initialized"
Expand All @@ -49,6 +51,21 @@ class MOBIUSCORE_API UFrameGrabberHelper : public UObject
TUniquePtr<FFrameGrabber> FrameGrabber;
#endif

#if PLATFORM_MAC
/** Try creating and removing a small probe file so macOS write denials are surfaced early. */
bool PreflightMacScreenshotWrite(const FString& DestPath, FString& OutFailureReason) const;

/** Handle the raw screenshot pixels so macOS writes go through our own code path. */
void HandleMacScreenshotCaptured(int32 InSizeX, int32 InSizeY, const TArray<FColor>& InImageData);

/** Reset macOS screenshot state and clear any timeout. */
void ResetMacCaptureState();

bool bMacScreenshotDelegateBound = false;
FString PendingOutputPath;
FTimerHandle MacCaptureTimeoutHandle;
#endif

FIntPoint TargetSize;
FIntPoint ViewportSize;
FIntPoint ConfiguredDownscaleSize = FIntPoint(800, 600);
Expand Down
Loading
Loading