// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= MetalRenderTarget.cpp: Metal render target implementation. =============================================================================*/ #include "MetalRHIPrivate.h" #include "ScreenRendering.h" #include "MetalCommandBuffer.h" #include "MetalCommandQueue.h" #include "MetalDynamicRHI.h" #include "MetalProfiler.h" #include "ResolveShader.h" #include "PipelineStateCache.h" #include "Math/PackedVector.h" #include "RHISurfaceDataConversion.h" static FResolveRect GetDefaultRect(const FResolveRect& Rect, uint32 DefaultWidth, uint32 DefaultHeight) { if (Rect.X1 >= 0 && Rect.X2 >= 0 && Rect.Y1 >= 0 && Rect.Y2 >= 0) { return Rect; } else { return FResolveRect(0, 0, DefaultWidth, DefaultHeight); } } int32 GMetalUseTexGetBytes = 1; static FAutoConsoleVariableRef CVarMetalUseTexGetBytes( TEXT("rhi.Metal.UseTexGetBytes"), GMetalUseTexGetBytes, TEXT("If true prefer using -[MTLTexture getBytes:...] to retreive texture data, creating a temporary shared/managed texture to copy from private texture storage when required, rather than using a temporary MTLBuffer. This works around data alignment bugs on some GPU vendor's drivers and may be more appropriate on iOS. (Default: True)"), ECVF_RenderThreadSafe ); /** Helper for accessing R10G10B10A2 colors. */ struct FMetalR10G10B10A2 { uint32 R : 10; uint32 G : 10; uint32 B : 10; uint32 A : 2; }; /** Helper for accessing R16G16 colors. */ struct FMetalRG16 { uint16 R; uint16 G; }; /** Helper for accessing R16G16B16A16 colors. */ struct FMetalRGBA16 { uint16 R; uint16 G; uint16 B; uint16 A; }; void FMetalDynamicRHI::RHIReadSurfaceData(FRHITexture* TextureRHI, FIntRect InRect, TArray& OutData, FReadSurfaceDataFlags InFlags) { // Use our current surface read implementation and convert to linear - should refactor to make optimal TArray OutDataUnConverted; RHIReadSurfaceData(TextureRHI, InRect, OutDataUnConverted, InFlags); OutData.SetNumUninitialized(OutDataUnConverted.Num()); for (uint32 i = 0; i < OutDataUnConverted.Num(); ++i) { OutData[i] = OutDataUnConverted[i].ReinterpretAsLinear(); } } static void ConvertSurfaceDataToFColor(EPixelFormat Format, uint32 Width, uint32 Height, uint8 *In, uint32 SrcPitch, FColor* Out, FReadSurfaceDataFlags InFlags) { bool bLinearToGamma = InFlags.GetLinearToGamma(); if (Format == PF_G16 || Format == PF_R16_UINT || Format == PF_R16_SINT) { ConvertRawR16DataToFColor(Width, Height, In, SrcPitch, Out); } else if (Format == PF_R8G8B8A8) { ConvertRawR8G8B8A8DataToFColor(Width, Height, In, SrcPitch, Out); } else if (Format == PF_B8G8R8A8) { ConvertRawB8G8R8A8DataToFColor(Width, Height, In, SrcPitch, Out); } else if (Format == PF_A2B10G10R10) { ConvertRawR10G10B10A2DataToFColor(Width, Height, In, SrcPitch, Out); } else if (Format == PF_FloatRGBA || Format == PF_PLATFORM_HDR_0) { ConvertRawR16G16B16A16FDataToFColor(Width, Height, In, SrcPitch, Out, bLinearToGamma); } else if (Format == PF_FloatR11G11B10) { ConvertRawR11G11B10DataToFColor(Width, Height, In, SrcPitch, Out, bLinearToGamma); } else if (Format == PF_A32B32G32R32F) { ConvertRawR32G32B32A32DataToFColor(Width, Height, In, SrcPitch, Out, bLinearToGamma); } else if (Format == PF_A16B16G16R16) { ConvertRawR16G16B16A16DataToFColor(Width, Height, In, SrcPitch, Out, bLinearToGamma); } else if (Format == PF_G16R16) { ConvertRawR16G16DataToFColor(Width, Height, In, SrcPitch, Out); } else if (Format == PF_DepthStencil) { ConvertRawD32S8DataToFColor(Width, Height, In, SrcPitch, Out, InFlags); } else { // not supported yet NOT_SUPPORTED("RHIReadSurfaceData Format"); } } void FMetalDynamicRHI::RHIReadSurfaceData(FRHITexture* TextureRHI, FIntRect Rect, TArray& OutData, FReadSurfaceDataFlags InFlags) { MTL_SCOPED_AUTORELEASE_POOL; // allocate output space const uint32 SizeX = Rect.Width(); const uint32 SizeY = Rect.Height(); OutData.SetNumUninitialized(SizeX * SizeY); if (!ensure(TextureRHI)) { FMemory::Memzero(OutData.GetData(), sizeof(FColor) * OutData.Num()); return; } FMetalSurface* Surface = GetMetalSurfaceFromRHITexture(TextureRHI); FColor* OutDataPtr = OutData.GetData(); MTL::Region Region(Rect.Min.X, Rect.Min.Y, SizeX, SizeY); MTLTexturePtr Texture = Surface->Texture; if(!Texture && EnumHasAnyFlags(Surface->GetDesc().Flags, TexCreate_Presentable)) { Texture = Surface->GetCurrentTexture(); } if(!Texture) { UE_LOG(LogRHI, Error, TEXT("Trying to read from an uninitialised texture.")); return; } if (GMetalUseTexGetBytes && Surface->GetDesc().Format != PF_DepthStencil && Surface->GetDesc().Format != PF_ShadowDepth) { SCOPE_CYCLE_COUNTER(STAT_MetalTexturePageOffTime); FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); MTLTexturePtr TempTexture; RHICmdList.EnqueueLambda([this, &Texture, &TempTexture, SizeX, SizeY, &Region, Surface, InFlags](FRHICommandListImmediate& RHICmdList) { FMetalRHICommandContext& Context = FMetalRHICommandContext::Get(RHICmdList); if (Texture->storageMode() == MTL::StorageModePrivate) { #if PLATFORM_MAC MTL::StorageMode StorageMode = MTL::StorageModeManaged; #else #if WITH_IOS_SIMULATOR MTL::StorageMode StorageMode = MTL::StorageModePrivate; #else MTL::StorageMode StorageMode = MTL::StorageModeShared; #endif #endif MTL::PixelFormat MetalFormat = (MTL::PixelFormat)GPixelFormats[Surface->GetDesc().Format].PlatformFormat; MTL::TextureDescriptor* Desc = MTL::TextureDescriptor::alloc()->init(); check(Desc); Desc->setTextureType(Texture->textureType()); Desc->setPixelFormat(Texture->pixelFormat()); Desc->setWidth(SizeX); Desc->setHeight(SizeY); Desc->setDepth(1); Desc->setMipmapLevelCount(1); // Only consider a single subresource and not the whole texture (like in the other RHIs) Desc->setSampleCount(Texture->sampleCount()); Desc->setArrayLength(Texture->arrayLength()); MTL::ResourceOptions GeneralResourceOption = (MTL::ResourceOptions)FMetalCommandQueue::GetCompatibleResourceOptions(MTL::ResourceOptions(((NS::UInteger)Texture->cpuCacheMode() << MTL::ResourceCpuCacheModeShift) | ((NS::UInteger)StorageMode << MTL::ResourceStorageModeShift) | MTL::ResourceHazardTrackingModeUntracked)); Desc->setResourceOptions(GeneralResourceOption); Desc->setCpuCacheMode(Texture->cpuCacheMode()); Desc->setStorageMode(StorageMode); Desc->setUsage(Texture->usage()); TempTexture = NS::TransferPtr(Device->GetDevice()->newTexture(Desc)); Desc->release(); Context.CopyFromTextureToTexture(Texture.get(), 0, InFlags.GetMip(), MTL::Origin(Region.origin), MTL::Size(Region.size), TempTexture.get(), 0, 0, MTL::Origin(0, 0, 0)); Texture = TempTexture; Region = MTL::Region(0, 0, SizeX, SizeY); } #if PLATFORM_MAC if(Texture->storageMode() == MTL::StorageModeManaged) { // Synchronise the texture with the CPU Context.SynchronizeTexture(Texture.get(), 0, InFlags.GetMip()); } #endif }); //kick the current command buffer. RHICmdList.SubmitAndBlockUntilGPUIdle(); const uint32 Stride = GPixelFormats[Surface->GetDesc().Format].BlockBytes * SizeX; const uint32 BytesPerImage = Stride * SizeY; TArray Data; Data.AddUninitialized(BytesPerImage); Texture->getBytes(Data.GetData(), Stride, BytesPerImage, Region, 0, 0); ConvertSurfaceDataToFColor(Surface->GetDesc().Format, SizeX, SizeY, (uint8*)Data.GetData(), Stride, OutDataPtr, InFlags); if (TempTexture) { FMetalDynamicRHI::Get().DeferredDelete(TempTexture); } } else { uint32 BytesPerPixel = (Surface->GetDesc().Format != PF_DepthStencil || !InFlags.GetOutputStencil()) ? GPixelFormats[Surface->GetDesc().Format].BlockBytes : 1; const uint32 Stride = BytesPerPixel * SizeX; const uint32 Alignment = PLATFORM_MAC ? 1u : 64u; // Mac permits natural row alignment (tightly-packed) but iOS does not. const uint32 AlignedStride = ((Stride - 1) & ~(Alignment - 1)) + Alignment; const uint32 BytesPerImage = AlignedStride * SizeY; FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); FMetalBufferPtr Buffer = Device->CreatePooledBuffer(FMetalPooledBufferArgs(Device, BytesPerImage, BUF_Dynamic, MTL::StorageModeShared)); RHICmdList.EnqueueLambda([this, &Buffer, &Texture, &Region, Surface, AlignedStride, BytesPerImage, InFlags](FRHICommandListImmediate& RHICmdList) { // Synchronise the texture with the CPU SCOPE_CYCLE_COUNTER(STAT_MetalTexturePageOffTime); FMetalRHICommandContext& Context = FMetalRHICommandContext::Get(RHICmdList); if (Surface->GetDesc().Format != PF_DepthStencil) { Context.CopyFromTextureToBuffer(Texture.get(), 0, InFlags.GetMip(), Region.origin, Region.size, Buffer, 0, AlignedStride, BytesPerImage, MTL::BlitOptionNone); } else { if (!InFlags.GetOutputStencil()) { Context.CopyFromTextureToBuffer(Texture.get(), 0, InFlags.GetMip(), Region.origin, Region.size, Buffer, 0, AlignedStride, BytesPerImage, MTL::BlitOptionDepthFromDepthStencil); } else { Context.CopyFromTextureToBuffer(Texture.get(), 0, InFlags.GetMip(), Region.origin, Region.size, Buffer, 0, AlignedStride, BytesPerImage, MTL::BlitOptionStencilFromDepthStencil); } } }); //kick the current command buffer. RHICmdList.SubmitAndBlockUntilGPUIdle(); ConvertSurfaceDataToFColor(Surface->GetDesc().Format, SizeX, SizeY, (uint8*)Buffer->Contents(), AlignedStride, OutDataPtr, InFlags); FMetalDynamicRHI::Get().DeferredDelete(Buffer); } } #if PLATFORM_IOS void FMetalDynamicRHI::RHIReadSurfaceDataDirect(FRHITexture* TextureRHI, FIntRect Rect, TArray& OutData) { if (!ensure(TextureRHI)) return; FMetalSurface* Surface = GetMetalSurfaceFromRHITexture(TextureRHI); // allocate output space const uint32 SizeX = Rect.Width(); const uint32 SizeY = Rect.Height(); OutData.SetNumUninitialized(SizeX * SizeY); MTLTexturePtr Texture = Surface->Texture; if(!Texture && EnumHasAnyFlags(Surface->GetDesc().Flags, TexCreate_Presentable)) { Texture = Surface->GetCurrentTexture(); } if(!Texture) { OutData.Empty(); UE_LOG(LogRHI, Error, TEXT("Trying to read from an uninitialised texture.")); return; } const uint32 Stride = GPixelFormats[Surface->GetDesc().Format].BlockBytes * SizeX; const uint32 BytesPerImage = Stride * SizeY; MTL::Region Region(Rect.Min.X, Rect.Min.Y, SizeX, SizeY); Texture->getBytes(OutData.GetData(), Stride, BytesPerImage, Region, 0, 0); } #endif // PLATFORM_IOS void FMetalDynamicRHI::RHIMapStagingSurface(FRHITexture* TextureRHI, FRHIGPUFence* FenceRHI, void*& OutData, int32& OutWidth, int32& OutHeight, uint32 GPUIndex) { MTL_SCOPED_AUTORELEASE_POOL; FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); RHICmdList.SubmitAndBlockUntilGPUIdle(); if (FenceRHI && !FenceRHI->Poll()) { ResourceCast(FenceRHI)->Wait(RHICmdList, FRHIGPUMask::FromIndex(GPUIndex)); } FMetalSurface* Surface = GetMetalSurfaceFromRHITexture(TextureRHI); FRHILockTextureResult Result = Surface->Lock(FRHILockTextureArgs::Lock2D(Surface, 0, RLM_ReadOnly, false), false); OutWidth = Surface->GetSizeX(); OutHeight = Surface->GetSizeY(); OutData = Result.Data; } void FMetalDynamicRHI::RHIUnmapStagingSurface(FRHITexture* TextureRHI, uint32 GPUIndex) { MTL_SCOPED_AUTORELEASE_POOL; FMetalSurface* Surface = GetMetalSurfaceFromRHITexture(TextureRHI); Surface->Unlock(FRHILockTextureArgs::Lock2D(Surface, 0, RLM_ReadOnly, false)); } void FMetalDynamicRHI::RHIReadSurfaceFloatData(FRHITexture* TextureRHI, FIntRect Rect, TArray& OutData, ECubeFace CubeFace,int32 ArrayIndex,int32 MipIndex) { MTL_SCOPED_AUTORELEASE_POOL; FMetalSurface* Surface = GetMetalSurfaceFromRHITexture(TextureRHI); MTLTexturePtr Texture = Surface->Texture; if(!Texture && EnumHasAnyFlags(Surface->GetDesc().Flags, TexCreate_Presentable)) { Texture = Surface->GetCurrentTexture(); } if(!Texture) { UE_LOG(LogRHI, Error, TEXT("Trying to read from an uninitialised texture.")); return; } // verify the input image format (but don't crash) if (Surface->GetDesc().Format != PF_FloatRGBA) { UE_LOG(LogRHI, Log, TEXT("Trying to read non-FloatRGBA surface.")); } if (TextureRHI->GetTextureCube()) { // adjust index to account for cubemaps as texture arrays ArrayIndex *= CubeFace_MAX; ArrayIndex += GetMetalCubeFace(CubeFace); } // allocate output space const uint32 SizeX = Rect.Width(); const uint32 SizeY = Rect.Height(); OutData.SetNumUninitialized(SizeX * SizeY); MTL::Region Region = MTL::Region(Rect.Min.X, Rect.Min.Y, SizeX, SizeY); // function wants details about the destination, not the source const uint32 Stride = GPixelFormats[Surface->GetDesc().Format].BlockBytes * SizeX; const uint32 Alignment = PLATFORM_MAC ? 1u : 64u; // Mac permits natural row alignment (tightly-packed) but iOS does not. const uint32 AlignedStride = ((Stride - 1) & ~(Alignment - 1)) + Alignment; const uint32 BytesPerImage = AlignedStride * SizeY; int32 FloatBGRADataSize = BytesPerImage; FMetalBufferPtr Buffer = Device->CreatePooledBuffer(FMetalPooledBufferArgs(Device, FloatBGRADataSize, BUF_Dynamic, MTL::StorageModeShared)); { // Synchronise the texture with the CPU SCOPE_CYCLE_COUNTER(STAT_MetalTexturePageOffTime); { FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); // Enqueue an RHI thread command to fully flush the GPU and write back caches RHICmdList.EnqueueLambda([&Texture, ArrayIndex, MipIndex, Region, &Buffer, AlignedStride, BytesPerImage](FRHICommandListImmediate& RHICmdList) { FMetalRHICommandContext& Context = FMetalRHICommandContext::Get(RHICmdList); Context.CopyFromTextureToBuffer(Texture.get(), ArrayIndex, MipIndex, Region.origin, Region.size, Buffer, 0, AlignedStride, BytesPerImage, MTL::BlitOptionNone); }); RHICmdList.SubmitAndBlockUntilGPUIdle(); } } uint8* DataPtr = (uint8*)Buffer->Contents(); FFloat16Color* OutDataPtr = OutData.GetData(); if (Alignment > 1u) { for (uint32 Row = 0; Row < SizeY; Row++) { FFloat16Color* FloatBGRAData = (FFloat16Color*)DataPtr; FMemory::Memcpy(OutDataPtr, FloatBGRAData, Stride); DataPtr += AlignedStride; OutDataPtr += SizeX; } } else { FFloat16Color* FloatBGRAData = (FFloat16Color*)DataPtr; FMemory::Memcpy(OutDataPtr, FloatBGRAData, FloatBGRADataSize); } FMetalDynamicRHI::Get().DeferredDelete(Buffer); } void FMetalDynamicRHI::RHIRead3DSurfaceFloatData(FRHITexture* TextureRHI,FIntRect InRect,FIntPoint ZMinMax,TArray& OutData) { MTL_SCOPED_AUTORELEASE_POOL; FMetalSurface* Surface = GetMetalSurfaceFromRHITexture(TextureRHI); MTL::Texture* Texture = Surface->Texture.get(); if(!Texture) { UE_LOG(LogRHI, Error, TEXT("Trying to read from an uninitialised texture.")); return; } // verify the input image format (but don't crash) if (Surface->GetDesc().Format != PF_FloatRGBA) { UE_LOG(LogRHI, Log, TEXT("Trying to read non-FloatRGBA surface.")); } // allocate output space const uint32 SizeX = InRect.Width(); const uint32 SizeY = InRect.Height(); const uint32 SizeZ = ZMinMax.Y - ZMinMax.X; OutData.SetNumUninitialized(SizeX * SizeY * SizeZ); MTL::Region Region = MTL::Region(InRect.Min.X, InRect.Min.Y, ZMinMax.X, SizeX, SizeY, SizeZ); // function wants details about the destination, not the source const uint32 Stride = GPixelFormats[Surface->GetDesc().Format].BlockBytes * SizeX; const uint32 Alignment = PLATFORM_MAC ? 1u : 64u; // Mac permits natural row alignment (tightly-packed) but iOS does not. const uint32 AlignedStride = ((Stride - 1) & ~(Alignment - 1)) + Alignment; const uint32 BytesPerImage = AlignedStride * SizeY; int32 FloatBGRADataSize = BytesPerImage * SizeZ; FMetalBufferPtr Buffer = Device->CreatePooledBuffer(FMetalPooledBufferArgs(Device, FloatBGRADataSize, BUF_Dynamic, MTL::StorageModeShared)); { // Synchronise the texture with the CPU SCOPE_CYCLE_COUNTER(STAT_MetalTexturePageOffTime); FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); RHICmdList.EnqueueLambda([this, &Buffer, &Texture, &Region, Surface, AlignedStride, BytesPerImage](FRHICommandListImmediate& RHICmdList) { FMetalRHICommandContext& Context = FMetalRHICommandContext::Get(RHICmdList); Context.CopyFromTextureToBuffer(Texture, 0, 0, Region.origin, Region.size, Buffer, 0, AlignedStride, BytesPerImage, MTL::BlitOptionNone); }); //kick the current command buffer. RHICmdList.SubmitAndBlockUntilGPUIdle(); } uint8* DataPtr = (uint8*)Buffer->Contents(); FFloat16Color* OutDataPtr = OutData.GetData(); if (Alignment > 1u) { for (uint32 Image = 0; Image < SizeZ; Image++) { for (uint32 Row = 0; Row < SizeY; Row++) { FFloat16Color* FloatBGRAData = (FFloat16Color*)DataPtr; FMemory::Memcpy(OutDataPtr, FloatBGRAData, Stride); DataPtr += AlignedStride; OutDataPtr += SizeX; } } } else { FFloat16Color* FloatBGRAData = (FFloat16Color*)DataPtr; FMemory::Memcpy(OutDataPtr, FloatBGRAData, FloatBGRADataSize); } FMetalDynamicRHI::Get().DeferredDelete(Buffer); }