Merge remote-tracking branch 'upstream/development' into ReturnCachedGemJsons
commit
e8cd6add51
@ -1,44 +0,0 @@
|
||||
"""
|
||||
Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
|
||||
UI Apps: AutomatedTesting.GameLauncher
|
||||
Launch AutomatedTesting.GameLauncher with Simple level
|
||||
Test should run in both gpu and non gpu
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import psutil
|
||||
|
||||
import ly_test_tools.environment.waiter as waiter
|
||||
import editor_python_test_tools.hydra_test_utils as editor_test_utils
|
||||
from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole
|
||||
from ly_remote_console.remote_console_commands import (
|
||||
send_command_and_expect_response as send_command_and_expect_response,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("launcher_platform", ["windows"])
|
||||
@pytest.mark.parametrize("project", ["AutomatedTesting"])
|
||||
@pytest.mark.parametrize("level", ["Simple"])
|
||||
@pytest.mark.SUITE_smoke
|
||||
class TestRemoteConsoleLoadLevelWorks(object):
|
||||
@pytest.fixture
|
||||
def remote_console_instance(self, request):
|
||||
console = RemoteConsole()
|
||||
|
||||
def teardown():
|
||||
if console.connected:
|
||||
console.stop()
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
return console
|
||||
|
||||
def test_RemoteConsole_LoadLevel_Works(self, launcher, level, remote_console_instance, launcher_platform):
|
||||
expected_lines = ['Level system is loading "Simple"']
|
||||
|
||||
editor_test_utils.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines, null_renderer=True)
|
||||
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e4937547ca4c486ef59656314401933217e0e0401fec103e1fb91c25ec60a177
|
||||
size 2806
|
||||
oid sha256:a5f9e27e0f22c31ca61d866fb594c6fde5b8ceb891e17dda075fa1e0033ec2b9
|
||||
size 1666
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
{
|
||||
"Type": "JsonSerialization",
|
||||
"Version": 1,
|
||||
"ClassName": "PassAsset",
|
||||
"ClassData": {
|
||||
"PassTemplate": {
|
||||
"Name": "LutGenerationTemplate",
|
||||
"PassClass": "LutGenerationPass",
|
||||
"Slots": [
|
||||
{
|
||||
"Name": "LutOutput",
|
||||
"SlotType": "Output",
|
||||
"ScopeAttachmentUsage": "RenderTarget",
|
||||
"LoadStoreAction": {
|
||||
"LoadAction": "DontCare"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ImageAttachments": [
|
||||
{
|
||||
"Name": "ColorGradingLut",
|
||||
"ImageDescriptor": {
|
||||
"Format": "R32G32B32A32_FLOAT",
|
||||
"BindFlags": [
|
||||
"ShaderWrite"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Connections": [
|
||||
{
|
||||
"LocalSlot": "LutOutput",
|
||||
"AttachmentRef": {
|
||||
"Pass": "This",
|
||||
"Attachment": "ColorGradingLut"
|
||||
}
|
||||
}
|
||||
],
|
||||
"PassData": {
|
||||
"$type": "FullscreenTrianglePassData",
|
||||
"ShaderAsset": {
|
||||
"FilePath": "Shaders/ColorGrading/LutGeneration.shader"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Atom/RPI/Math.azsli>
|
||||
#include <Atom/Features/ColorManagement/TransformColor.azsli>
|
||||
#include <Atom/Features/PostProcessing/AcesColorSpaceConversion.azsli>
|
||||
#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli>
|
||||
#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli>
|
||||
#include <3rdParty/Features/PostProcessing/KelvinToRgb.azsli>
|
||||
|
||||
static const float FloatEpsilon = 1.192092896e-07; // 1.0 + FloatEpsilon != 1.0, smallest positive float
|
||||
static const float FloatMin = FLOAT_32_MIN; // Min float number that is positive
|
||||
static const float FloatMax = FLOAT_32_MAX; // Max float number representable
|
||||
|
||||
static const float AcesCcMidGrey = 0.4135884;
|
||||
|
||||
float SaturateWithEpsilon(float value)
|
||||
{
|
||||
return clamp(value, FloatEpsilon, 1.0f);
|
||||
}
|
||||
|
||||
// Below are the color grading functions. These expect the frame color to be in ACEScg space.
|
||||
// Note that some functions may have some quirks in their implementation and is subject to change.
|
||||
float3 ColorGradePostExposure (float3 frameColor, float exposure)
|
||||
{
|
||||
frameColor *= pow(2.0f, exposure);
|
||||
return frameColor;
|
||||
}
|
||||
|
||||
// The contrast equation is performed in ACEScc (logarithmic) color space.
|
||||
float3 ColorGradingContrast (float3 frameColor, float midgrey, float amount)
|
||||
{
|
||||
const float contrastAdjustment = amount * 0.01f + 1.0f;
|
||||
frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScg, ColorSpaceId::ACEScc);
|
||||
frameColor = (frameColor - midgrey) * contrastAdjustment + midgrey;
|
||||
return frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScc, ColorSpaceId::ACEScg);
|
||||
}
|
||||
|
||||
// The swatchColor param expects a linear RGB value.
|
||||
float3 ColorGradeColorFilter (float3 frameColor, float3 swatchColor, float alpha, float colorFilterIntensity)
|
||||
{
|
||||
swatchColor = TransformColor(swatchColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
|
||||
swatchColor *= pow(2.0f, colorFilterIntensity);
|
||||
const float3 frameAdjust = frameColor * swatchColor;
|
||||
return frameColor = lerp(frameColor, frameAdjust, alpha);
|
||||
}
|
||||
|
||||
float3 ColorGradeHueShift (float3 frameColor, float amount)
|
||||
{
|
||||
float3 frameHsv = RgbToHsv(frameColor);
|
||||
const float hue = frameHsv.x + amount;
|
||||
frameHsv.x = RotateHue(hue, 0.0, 1.0);
|
||||
return HsvToRgb(frameHsv);
|
||||
}
|
||||
|
||||
float3 ColorGradeSaturation (float3 frameColor, float control)
|
||||
{
|
||||
const float vLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
|
||||
return (frameColor - vLuminance) * control + vLuminance;
|
||||
}
|
||||
|
||||
float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin)
|
||||
{
|
||||
const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
|
||||
const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
|
||||
const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL
|
||||
return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance
|
||||
}
|
||||
|
||||
// pow(f, e) won't work if f is negative, or may cause inf/NAN.
|
||||
float3 NoNanPow(float3 base, float3 power)
|
||||
{
|
||||
return pow(max(abs(base), float3(FloatEpsilon, FloatEpsilon, FloatEpsilon)), power);
|
||||
}
|
||||
|
||||
float3 ColorGradeSplitTone (
|
||||
float3 frameColor,
|
||||
float balance,
|
||||
float weight,
|
||||
float3 splitToneShadowsColor,
|
||||
float3 splitToneHighlightsColor)
|
||||
{
|
||||
float3 frameSplitTone = NoNanPow(frameColor, 1.0 / 2.2);
|
||||
const float t = SaturateWithEpsilon(CalculateLuminance(SaturateWithEpsilon(frameSplitTone), ColorSpaceId::ACEScg) + balance);
|
||||
const float3 shadows = lerp(0.5, splitToneShadowsColor, 1.0 - t);
|
||||
const float3 highlights = lerp(0.5, splitToneHighlightsColor, t);
|
||||
frameSplitTone = BlendMode_SoftLight(frameSplitTone, shadows);
|
||||
frameSplitTone = BlendMode_SoftLight(frameSplitTone, highlights);
|
||||
frameSplitTone = NoNanPow(frameSplitTone, 2.2);
|
||||
return lerp(frameColor.rgb, frameSplitTone.rgb, weight);
|
||||
}
|
||||
|
||||
float3 ColorGradeChannelMixer (
|
||||
float3 frameColor,
|
||||
float3 channelMixingRed,
|
||||
float3 channelMixingGreen,
|
||||
float3 channelMixingBlue)
|
||||
{
|
||||
return mul(float3x3(channelMixingRed,
|
||||
channelMixingGreen,
|
||||
channelMixingBlue),
|
||||
frameColor);
|
||||
}
|
||||
|
||||
float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStart, float shadowsEnd,
|
||||
float highlightsStart, float highlightsEnd, float weight,
|
||||
float4 shadowsColor, float4 midtonesColor, float4 highlightsColor)
|
||||
{
|
||||
const float3 shadowsColorACEScg = TransformColor(shadowsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
|
||||
const float3 midtonesColorACEScg = TransformColor(midtonesColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
|
||||
const float3 highlightsColorACEScg = TransformColor(highlightsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
|
||||
|
||||
const float cLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
|
||||
const float shadowsWeight = 1.0 - smoothstep(shadowsStart, shadowsEnd, cLuminance);
|
||||
const float highlightsWeight = smoothstep(highlightsStart, highlightsEnd, cLuminance);
|
||||
const float midtonesWeight = 1.0 - shadowsWeight - highlightsWeight;
|
||||
|
||||
const float3 frameSmh = frameColor * shadowsColorACEScg * shadowsWeight +
|
||||
frameColor * midtonesColorACEScg * midtonesWeight +
|
||||
frameColor * highlightsColorACEScg * highlightsWeight;
|
||||
return lerp(frameColor.rgb, frameSmh.rgb, weight);
|
||||
}
|
||||
|
||||
// perform color grading in ACEScg space
|
||||
float3 ColorGrade(float3 frameColor)
|
||||
{
|
||||
frameColor = lerp(frameColor, ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure), PassSrg::m_colorAdjustmentWeight);
|
||||
frameColor = lerp(frameColor, ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin), PassSrg::m_whiteBalanceWeight);
|
||||
frameColor = lerp(frameColor, ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast), PassSrg::m_colorAdjustmentWeight);
|
||||
frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb,
|
||||
PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight);
|
||||
frameColor = max(frameColor, 0.0);
|
||||
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight);
|
||||
|
||||
frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight,
|
||||
PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor);
|
||||
frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue);
|
||||
frameColor = max(frameColor, 0.0);
|
||||
frameColor = ColorGradeShadowsMidtonesHighlights(frameColor, PassSrg::m_smhShadowsStart, PassSrg::m_smhShadowsEnd,
|
||||
PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight,
|
||||
PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor);
|
||||
|
||||
|
||||
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight);
|
||||
frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight);
|
||||
return max(frameColor.rgb, 0.0);
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Atom/RPI/Math.azsli>
|
||||
|
||||
#include <Atom/Features/SrgSemantics.azsli>
|
||||
|
||||
#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
|
||||
|
||||
#include <Atom/Features/PostProcessing/Aces.azsli>
|
||||
#include <Atom/Features/PostProcessing/Shapers.azsli>
|
||||
|
||||
float3 convert2Dto3DLutCoords(float2 uv, float width, float height)
|
||||
{
|
||||
// convert from center pixel uvs to [0,1]
|
||||
float offset = 1.0/height/2.0;
|
||||
float scale = 1.0 - offset*2.0;
|
||||
|
||||
float2 adjustedUv = float2(uv.x * width, uv.y * height);
|
||||
float3 coords = float3(adjustedUv.x%height, 0.5 + int(adjustedUv.x/height), adjustedUv.y)/height;
|
||||
return (coords - offset)/scale;
|
||||
}
|
||||
|
||||
ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
|
||||
{
|
||||
// framebuffer sampler
|
||||
Sampler PointSampler
|
||||
{
|
||||
MinFilter = Point;
|
||||
MagFilter = Point;
|
||||
MipFilter = Point;
|
||||
AddressU = Clamp;
|
||||
AddressV = Clamp;
|
||||
AddressW = Clamp;
|
||||
};
|
||||
|
||||
int m_lutResolution;
|
||||
int m_shaperType;
|
||||
float m_shaperBias;
|
||||
float m_shaperScale;
|
||||
|
||||
float m_colorAdjustmentWeight;
|
||||
float m_colorGradingExposure;
|
||||
float m_colorGradingContrast;
|
||||
float m_colorGradingPreSaturation;
|
||||
float m_colorFilterIntensity;
|
||||
float m_colorFilterMultiply;
|
||||
float4 m_colorFilterSwatch;
|
||||
|
||||
float m_whiteBalanceWeight;
|
||||
float m_whiteBalanceKelvin;
|
||||
float m_whiteBalanceTint;
|
||||
|
||||
float m_splitToneBalance;
|
||||
float m_splitToneWeight;
|
||||
float4 m_splitToneShadowsColor;
|
||||
float4 m_splitToneHighlightsColor;
|
||||
|
||||
float m_smhShadowsStart;
|
||||
float m_smhShadowsEnd;
|
||||
float m_smhHighlightsStart;
|
||||
float m_smhHighlightsEnd;
|
||||
float m_smhWeight;
|
||||
float4 m_smhShadowsColor;
|
||||
float4 m_smhMidtonesColor;
|
||||
float4 m_smhHighlightsColor;
|
||||
|
||||
float3 m_channelMixingRed;
|
||||
float3 m_channelMixingGreen;
|
||||
float3 m_channelMixingBlue;
|
||||
|
||||
float m_finalAdjustmentWeight;
|
||||
float m_colorGradingPostSaturation;
|
||||
float m_colorGradingHueShift;
|
||||
}
|
||||
|
||||
#include <Atom/Features/PostProcessing/HDRColorGradingCommon.azsl>
|
||||
|
||||
struct PSOutput
|
||||
{
|
||||
float4 m_lutOutput : SV_Target0;
|
||||
};
|
||||
|
||||
PSOutput MainPS(VSOutput IN)
|
||||
{
|
||||
ShaperType shaperType = (ShaperType)PassSrg::m_shaperType;
|
||||
int lutResolution = PassSrg::m_lutResolution;
|
||||
|
||||
PSOutput OUT;
|
||||
|
||||
// baseCoords are from 0-1
|
||||
float3 baseCoords = convert2Dto3DLutCoords(IN.m_texCoord, lutResolution*lutResolution, lutResolution);
|
||||
|
||||
float3 linearColor = ShaperToLinear(baseCoords, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
|
||||
|
||||
linearColor = TransformColor(linearColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
|
||||
float3 gradedColor = ColorGrade(linearColor);
|
||||
gradedColor = TransformColor(gradedColor, ColorSpaceId::ACEScg, ColorSpaceId::LinearSRGB);
|
||||
|
||||
// Bring back coordinates into 0-1
|
||||
float3 shapedColor = LinearToShaper(gradedColor, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
|
||||
shapedColor = saturate(shapedColor);
|
||||
|
||||
OUT.m_lutOutput = float4(shapedColor, 1.0);
|
||||
return OUT;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
{
|
||||
"Source" : "LutGeneration",
|
||||
|
||||
"DepthStencilState" : {
|
||||
"Depth" : { "Enable" : false }
|
||||
},
|
||||
|
||||
"ProgramSettings":
|
||||
{
|
||||
"EntryPoints":
|
||||
[
|
||||
{
|
||||
"name": "MainVS",
|
||||
"type": "Vertex"
|
||||
},
|
||||
{
|
||||
"name": "MainPS",
|
||||
"type": "Fragment"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
enum class LutResolution
|
||||
{
|
||||
Lut16x16x16 = 16,
|
||||
Lut32x32x32 = 32,
|
||||
Lut64x64x64 = 64
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ColorGrading/LutGenerationPass.h>
|
||||
#include <Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h>
|
||||
|
||||
#include <Atom/RPI.Public/Scene.h>
|
||||
#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
|
||||
RPI::Ptr<LutGenerationPass> LutGenerationPass::Create(const RPI::PassDescriptor& descriptor)
|
||||
{
|
||||
RPI::Ptr<LutGenerationPass> pass = aznew LutGenerationPass(descriptor);
|
||||
return AZStd::move(pass);
|
||||
}
|
||||
|
||||
LutGenerationPass::LutGenerationPass(const RPI::PassDescriptor& descriptor)
|
||||
: HDRColorGradingPass(descriptor)
|
||||
{
|
||||
}
|
||||
|
||||
void LutGenerationPass::InitializeInternal()
|
||||
{
|
||||
HDRColorGradingPass::InitializeInternal();
|
||||
|
||||
m_lutResolutionIndex.Reset();
|
||||
m_lutShaperTypeIndex.Reset();
|
||||
m_lutShaperScaleIndex.Reset();
|
||||
}
|
||||
|
||||
void LutGenerationPass::FrameBeginInternal(FramePrepareParams params)
|
||||
{
|
||||
const auto* colorGradingSettings = GetHDRColorGradingSettings();
|
||||
if (colorGradingSettings)
|
||||
{
|
||||
m_shaderResourceGroup->SetConstant(m_lutResolutionIndex, colorGradingSettings->GetLutResolution());
|
||||
|
||||
auto shaperParams = AcesDisplayMapperFeatureProcessor::GetShaperParameters(
|
||||
colorGradingSettings->GetShaperPresetType(),
|
||||
colorGradingSettings->GetCustomMinExposure(),
|
||||
colorGradingSettings->GetCustomMaxExposure());
|
||||
m_shaderResourceGroup->SetConstant(m_lutShaperTypeIndex, shaperParams.m_type);
|
||||
m_shaderResourceGroup->SetConstant(m_lutShaperBiasIndex, shaperParams.m_bias);
|
||||
m_shaderResourceGroup->SetConstant(m_lutShaperScaleIndex, shaperParams.m_scale);
|
||||
}
|
||||
|
||||
HDRColorGradingPass::FrameBeginInternal(params);
|
||||
}
|
||||
|
||||
void LutGenerationPass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context)
|
||||
{
|
||||
const auto* colorGradingSettings = GetHDRColorGradingSettings();
|
||||
if (colorGradingSettings)
|
||||
{
|
||||
uint32_t lutResolution = aznumeric_cast<uint32_t>(colorGradingSettings->GetLutResolution());
|
||||
RHI::Size lutSize{ lutResolution * lutResolution, lutResolution, 1 };
|
||||
|
||||
RPI::Ptr<RPI::PassAttachment> attachment = FindOwnedAttachment(Name{ "ColorGradingLut" });
|
||||
RHI::ImageDescriptor& imageDescriptor = attachment->m_descriptor.m_image;
|
||||
imageDescriptor.m_size = lutSize;
|
||||
SetViewportScissorFromImageSize(lutSize);
|
||||
}
|
||||
HDRColorGradingPass::BuildCommandListInternal(context);
|
||||
}
|
||||
|
||||
bool LutGenerationPass::IsEnabled() const
|
||||
{
|
||||
const auto* colorGradingSettings = GetHDRColorGradingSettings();
|
||||
return colorGradingSettings ? colorGradingSettings->GetGenerateLut() : false;
|
||||
}
|
||||
|
||||
void LutGenerationPass::SetViewportScissorFromImageSize(const RHI::Size& imageSize)
|
||||
{
|
||||
const RHI::Viewport viewport(0.f, imageSize.m_width * 1.f, 0.f, imageSize.m_height * 1.f);
|
||||
const RHI::Scissor scissor(0, 0, imageSize.m_width, imageSize.m_height);
|
||||
m_viewportState = viewport;
|
||||
m_scissorState = scissor;
|
||||
}
|
||||
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <PostProcessing/HDRColorGradingPass.h>
|
||||
#include <Atom/RPI.Public/Shader/Shader.h>
|
||||
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
|
||||
#include <Atom/RPI.Reflect/Shader/ShaderVariantKey.h>
|
||||
|
||||
#include <PostProcess/ColorGrading/HDRColorGradingSettings.h>
|
||||
#include <Atom/Feature/DisplayMapper/DisplayMapperFeatureProcessorInterface.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
// Performs color grading on an identity LUT strip
|
||||
class LutGenerationPass
|
||||
: public AZ::Render::HDRColorGradingPass
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(LutGenerationPass, "{C21DABA8-B538-4C80-BA18-5B97CC9259E5}", AZ::RPI::FullscreenTrianglePass);
|
||||
AZ_CLASS_ALLOCATOR(LutGenerationPass, SystemAllocator, 0);
|
||||
|
||||
virtual ~LutGenerationPass() = default;
|
||||
|
||||
//! Creates a ColorGradingPass
|
||||
static RPI::Ptr<LutGenerationPass> Create(const RPI::PassDescriptor& descriptor);
|
||||
|
||||
protected:
|
||||
LutGenerationPass(const RPI::PassDescriptor& descriptor);
|
||||
|
||||
void InitializeInternal() override;
|
||||
void FrameBeginInternal(FramePrepareParams params) override;
|
||||
void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override;
|
||||
bool IsEnabled() const override;
|
||||
private:
|
||||
// Set viewport scissor based on output LUT resolution
|
||||
void SetViewportScissorFromImageSize(const RHI::Size& imageSize);
|
||||
|
||||
RHI::ShaderInputNameIndex m_lutResolutionIndex = "m_lutResolution";
|
||||
RHI::ShaderInputNameIndex m_lutShaperTypeIndex = "m_shaperType";
|
||||
RHI::ShaderInputNameIndex m_lutShaperBiasIndex = "m_shaperBias";
|
||||
RHI::ShaderInputNameIndex m_lutShaperScaleIndex = "m_shaperScale";
|
||||
};
|
||||
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
@ -0,0 +1,89 @@
|
||||
"""
|
||||
Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
"""
|
||||
|
||||
"""
|
||||
This script is used by the Editor HDR Color Grading component to use a stored lut asset path
|
||||
and pass it onto a Look Modification Component.
|
||||
|
||||
The HDR Color Grading component will be disabled as it is not compatible with the Look
|
||||
Modification Component.
|
||||
"""
|
||||
import azlmbr
|
||||
import azlmbr.legacy.general as general
|
||||
|
||||
LOOK_MODIFICATION_LUT_PROPERTY_PATH = 'Controller|Configuration|Color Grading LUT'
|
||||
LOOK_MODIFICATION_ENABLE_PROPERTY_PATH = 'Controller|Configuration|Enable look modification'
|
||||
COLOR_GRADING_COMPONENT_ID = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["HDR Color Grading"], 0)
|
||||
LOOK_MODIFICATION_COMPONENT_ID = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Look Modification"], 0)
|
||||
|
||||
|
||||
def disable_hdr_color_grading_component(entity_id):
|
||||
componentOutcome = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'GetComponentOfType', entity_id, COLOR_GRADING_COMPONENT_ID[0])
|
||||
if(componentOutcome.IsSuccess()):
|
||||
azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'DisableComponents', [componentOutcome.GetValue()])
|
||||
|
||||
def add_look_modification_component(entity_id):
|
||||
componentOutcome = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'AddComponentsOfType', entity_id, LOOK_MODIFICATION_COMPONENT_ID)
|
||||
return componentOutcome.GetValue()[0]
|
||||
|
||||
def get_look_modification_component(entity_id):
|
||||
componentOutcome = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'GetComponentOfType', entity_id, LOOK_MODIFICATION_COMPONENT_ID[0])
|
||||
if componentOutcome.IsSuccess():
|
||||
return componentOutcome.GetValue()
|
||||
else:
|
||||
return None
|
||||
|
||||
def activate_look_modification_lut(look_modification_component, asset_relative_path):
|
||||
print(asset_relative_path)
|
||||
asset_id = azlmbr.asset.AssetCatalogRequestBus(
|
||||
azlmbr.bus.Broadcast,
|
||||
'GetAssetIdByPath',
|
||||
asset_relative_path,
|
||||
azlmbr.math.Uuid(),
|
||||
False
|
||||
)
|
||||
azlmbr.editor.EditorComponentAPIBus(
|
||||
azlmbr.bus.Broadcast,
|
||||
'SetComponentProperty',
|
||||
look_modification_component,
|
||||
LOOK_MODIFICATION_LUT_PROPERTY_PATH,
|
||||
asset_id
|
||||
)
|
||||
azlmbr.editor.EditorComponentAPIBus(
|
||||
azlmbr.bus.Broadcast,
|
||||
'SetComponentProperty',
|
||||
look_modification_component,
|
||||
LOOK_MODIFICATION_ENABLE_PROPERTY_PATH,
|
||||
True
|
||||
)
|
||||
|
||||
def activate_lut_asset(entity_id, asset_relative_path):
|
||||
disable_hdr_color_grading_component(entity_id)
|
||||
|
||||
look_modification_component = get_look_modification_component(entity_id)
|
||||
if not look_modification_component:
|
||||
look_modification_component = add_look_modification_component(entity_id)
|
||||
|
||||
general.idle_wait_frames(5)
|
||||
|
||||
if look_modification_component:
|
||||
activate_look_modification_lut(look_modification_component, asset_relative_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('--entityName', type=str, required=True, help='Entity ID to manage')
|
||||
parser.add_argument('--assetRelativePath', type=str, required=True, help='Lut asset relative path to activate')
|
||||
args=parser.parse_args()
|
||||
|
||||
# Get the entity id
|
||||
searchFilter = azlmbr.entity.SearchFilter()
|
||||
searchFilter.names = [args.entityName]
|
||||
entityIdList = azlmbr.entity.SearchBus(azlmbr.bus.Broadcast, 'SearchEntities', searchFilter)
|
||||
|
||||
for entityId in entityIdList:
|
||||
activate_lut_asset(entityId, args.assetRelativePath)
|
||||
@ -0,0 +1,63 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
import numpy as np
|
||||
import logging as _logging
|
||||
from ColorGrading import get_uv_coord
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
_MODULENAME = 'ColorGrading.azasset_converter_utils'
|
||||
|
||||
_LOGGER = _logging.getLogger(_MODULENAME)
|
||||
_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME}))
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
""" Utility functions for generating LUT azassets """
|
||||
def generate_lut_values(image_spec, image_buffer):
|
||||
lut_size = image_spec.height
|
||||
|
||||
lut_intervals = []
|
||||
lut_values = []
|
||||
|
||||
# First line contains the vertex intervals
|
||||
dv = 1023.0 / float(lut_size-1)
|
||||
for i in range(lut_size):
|
||||
lut_intervals.append(np.uint16(dv * i))
|
||||
# Texels are in R G B per line with indices increasing first with blue, then green, and then red.
|
||||
for r in range(lut_size):
|
||||
for g in range(lut_size):
|
||||
for b in range(lut_size):
|
||||
uv = get_uv_coord(lut_size, r, g, b)
|
||||
px = np.array(image_buffer.getpixel(uv[0], uv[1]), dtype='f')
|
||||
px = np.clip(px, 0.0, 1.0)
|
||||
px = np.uint16(px * 4095)
|
||||
lut_values.append(px)
|
||||
|
||||
return lut_intervals, lut_values
|
||||
|
||||
# To Do: add some input file validation
|
||||
# If the input file doesn't exist, you'll get a LUT with res of 0 x 0 and result in a math error
|
||||
#Resolution is 0 x 0
|
||||
#writing C:\Depot\o3de-engine\Gems\AtomLyIntegration\CommonFeatures\Tools\ColorGrading\TestData\Nuke\HDR\Nuke_Post_grade_LUT.3dl...
|
||||
#Traceback (most recent call last):
|
||||
#File "..\..\Editor\Scripts\ColorGrading\exr_to_3dl_azasset.py", line 103, in <module>
|
||||
#dv = 1023.0 / float(lutSize)
|
||||
# ZeroDivisionError: float division by zero
|
||||
|
||||
def write_3DL(file_path, lut_size, lut_intervals, lut_values):
|
||||
lut_file_path = f'{file_path}.3dl'
|
||||
_LOGGER.info(f"Writing {lut_file_path}...")
|
||||
lut_file = open(lut_file_path, 'w')
|
||||
for i in range(lut_size):
|
||||
lut_file.write(f"{lut_intervals[i]} ")
|
||||
lut_file.write("\n")
|
||||
for px in lut_values:
|
||||
lut_file.write(f"{px[0]} {px[1]} {px[2]}\n")
|
||||
lut_file.close()
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
# coding:utf-8
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
"""
|
||||
input: a shaped .tiff representing a LUT (for instance coming out of photoshop)
|
||||
output: a inverse shaped LUT as .tiff
|
||||
^ as a .3DL (normalized lut file type)
|
||||
^ as a .azasset (for o3de engine)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import math
|
||||
import site
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
import logging as _logging
|
||||
import numpy as np
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
_MODULENAME = 'ColorGrading.tiff_to_3dl_azasset'
|
||||
|
||||
_LOGGER = _logging.getLogger(_MODULENAME)
|
||||
_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME}))
|
||||
|
||||
import ColorGrading.initialize
|
||||
if ColorGrading.initialize.start():
|
||||
try:
|
||||
import OpenImageIO as oiio
|
||||
pass
|
||||
except ImportError as e:
|
||||
_LOGGER.error(f"invalid import: {e}")
|
||||
sys.exit(1)
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
from ColorGrading.from_3dl_to_azasset import write_azasset
|
||||
|
||||
from ColorGrading.azasset_converter_utils import generate_lut_values, write_3DL
|
||||
|
||||
###########################################################################
|
||||
# Main Code Block, runs this script as main (testing)
|
||||
# -------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
"""Run this file as main"""
|
||||
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('--i', type=str, required=True, help='input file')
|
||||
parser.add_argument('--o', type=str, required=True, help='output file')
|
||||
args=parser.parse_args()
|
||||
|
||||
# Read input image
|
||||
image_buffer=oiio.ImageBuf(args.i)
|
||||
image_spec=image_buffer.spec()
|
||||
|
||||
#img = oiio.ImageInput.open(args.i)
|
||||
#_LOGGER.info(f"Resolution is, x: {img.spec().width} and y: {img.spec().height}")
|
||||
|
||||
_LOGGER.info(f"Resolution is, x: {image_buffer.spec().width} and y: {image_buffer.spec().height}")
|
||||
|
||||
if image_spec.width != image_spec.height * image_spec.height:
|
||||
_LOGGER.info(f"invalid input file dimensions. Expect lengthwise LUT with dimension W: s*s X H: s, where s is the size of the LUT")
|
||||
sys.exit(1)
|
||||
|
||||
lut_intervals, lut_values = generate_lut_values(image_spec, image_buffer)
|
||||
|
||||
write_3DL(args.o, image_spec.height, lut_intervals, lut_values)
|
||||
|
||||
# write_azasset(file_path, lut_intervals, lut_values, azasset_json=AZASSET_LUT)
|
||||
write_azasset(args.o, lut_intervals, lut_values)
|
||||
|
||||
# example from command line
|
||||
# python % DCCSI_COLORGRADING_SCRIPTS %\lut_helper.py - -i C: \Depot\o3de\Gems\Atom\Feature\Common\Tools\ColorGrading\Resources\LUTs\linear_32_LUT.tiff - -op pre - grading - -shaper Log2 - 48nits - -o C: \Depot\o3de\Gems\Atom\Feature\Common\Tools\ColorGrading\Resources\LUTs\base_Log2-48nits_32_LUT.exr
|
||||
@ -0,0 +1,16 @@
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Scripts/ColorGrading/__init__.py
|
||||
Scripts/ColorGrading/initialize.py
|
||||
Scripts/ColorGrading/azasset_converter_utils.py
|
||||
Scripts/ColorGrading/from_3dl_to_azasset.py
|
||||
Scripts/ColorGrading/tiff_to_3dl_azasset.py
|
||||
Scripts/ColorGrading/activate_lut_asset.py
|
||||
)
|
||||
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Debug/EventTrace.h>
|
||||
#include <AzCore/Debug/Profiler.h>
|
||||
#include <AzCore/RTTI/RTTI.h>
|
||||
#include <AzCore/std/containers/ring_buffer.h>
|
||||
#include <AzCore/std/containers/unordered_map.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace RHI
|
||||
{
|
||||
//! Structure that is used to cache a timed region into the thread's local storage.
|
||||
struct CachedTimeRegion
|
||||
{
|
||||
//! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region
|
||||
//! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown.
|
||||
struct GroupRegionName
|
||||
{
|
||||
GroupRegionName() = delete;
|
||||
GroupRegionName(const char* const group, const char* const region);
|
||||
|
||||
const char* m_groupName = nullptr;
|
||||
const char* m_regionName = nullptr;
|
||||
|
||||
struct Hash
|
||||
{
|
||||
AZStd::size_t operator()(const GroupRegionName& name) const;
|
||||
};
|
||||
bool operator==(const GroupRegionName& other) const;
|
||||
};
|
||||
|
||||
CachedTimeRegion() = default;
|
||||
CachedTimeRegion(const GroupRegionName& groupRegionName);
|
||||
CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick);
|
||||
|
||||
GroupRegionName m_groupRegionName{nullptr, nullptr};
|
||||
|
||||
uint16_t m_stackDepth = 0u;
|
||||
AZStd::sys_time_t m_startTick = 0;
|
||||
AZStd::sys_time_t m_endTick = 0;
|
||||
};
|
||||
|
||||
//! Interface class of the CpuProfiler
|
||||
class CpuProfiler
|
||||
{
|
||||
public:
|
||||
using ThreadTimeRegionMap = AZStd::unordered_map<AZStd::string, AZStd::vector<CachedTimeRegion>>;
|
||||
using TimeRegionMap = AZStd::unordered_map<AZStd::thread_id, ThreadTimeRegionMap>;
|
||||
|
||||
AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}");
|
||||
|
||||
CpuProfiler() = default;
|
||||
virtual ~CpuProfiler() = default;
|
||||
|
||||
AZ_DISABLE_COPY_MOVE(CpuProfiler);
|
||||
|
||||
static CpuProfiler* Get();
|
||||
|
||||
//! Get the last frame's TimeRegionMap
|
||||
virtual const TimeRegionMap& GetTimeRegionMap() const = 0;
|
||||
|
||||
//! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called.
|
||||
[[nodiscard]] virtual bool BeginContinuousCapture() = 0;
|
||||
|
||||
//! Flush the CPU Profiler's saved data into the passed ring buffer .
|
||||
[[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget) = 0;
|
||||
|
||||
virtual bool IsContinuousCaptureInProgress() const = 0;
|
||||
|
||||
//! Enable/Disable the CpuProfiler
|
||||
virtual void SetProfilerEnabled(bool enabled) = 0;
|
||||
|
||||
virtual bool IsProfilerEnabled() const = 0 ;
|
||||
};
|
||||
|
||||
} // namespace RPI
|
||||
} // namespace AZ
|
||||
@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <Atom/RHI.Reflect/Base.h>
|
||||
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/Memory/OSAllocator.h>
|
||||
#include <AzCore/Name/Name.h>
|
||||
#include <AzCore/std/containers/map.h>
|
||||
#include <AzCore/std/containers/unordered_set.h>
|
||||
#include <AzCore/std/parallel/mutex.h>
|
||||
#include <AzCore/std/parallel/shared_mutex.h>
|
||||
#include <AzCore/std/smart_ptr/intrusive_refcount.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace RHI
|
||||
{
|
||||
//! Thread local class to keep track of the thread's cached time regions.
|
||||
//! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl.
|
||||
//! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage.
|
||||
class CpuTimingLocalStorage :
|
||||
public AZStd::intrusive_refcount<AZStd::atomic_uint>
|
||||
{
|
||||
friend class CpuProfilerImpl;
|
||||
|
||||
public:
|
||||
AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0);
|
||||
|
||||
CpuTimingLocalStorage();
|
||||
~CpuTimingLocalStorage();
|
||||
|
||||
private:
|
||||
// Maximum stack size
|
||||
static constexpr uint32_t TimeRegionStackSize = 2048u;
|
||||
|
||||
// Adds a region to the stack, gets called each time a region begins
|
||||
void RegionStackPushBack(CachedTimeRegion& timeRegion);
|
||||
|
||||
// Pops a region from the stack, gets called each time a region ends
|
||||
void RegionStackPopBack();
|
||||
|
||||
// Add a new cached time region. If the stack is empty, flush all entries to the cached map
|
||||
void AddCachedRegion(const CachedTimeRegion& timeRegionCached);
|
||||
|
||||
// Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked
|
||||
void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap);
|
||||
|
||||
AZStd::thread_id m_executingThreadId;
|
||||
// Keeps track of the current thread's stack depth
|
||||
uint32_t m_stackLevel = 0u;
|
||||
|
||||
// Cached region map, will be flushed to the system's map when the system requests it
|
||||
CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap;
|
||||
|
||||
// Use fixed vectors to avoid re-allocating new elements
|
||||
// Keeps track of the regions that added and removed using the macro
|
||||
AZStd::fixed_vector<CachedTimeRegion, TimeRegionStackSize> m_timeRegionStack;
|
||||
|
||||
// Keeps track of regions that completed (i.e regions that was pushed and popped from the stack)
|
||||
// Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be
|
||||
// copied to the map.
|
||||
AZStd::fixed_vector<CachedTimeRegion, TimeRegionStackSize> m_cachedTimeRegions;
|
||||
AZStd::mutex m_cachedTimeRegionMutex;
|
||||
|
||||
// Dirty flag which is set when the CpuProfiler's enabled state is set from false to true
|
||||
AZStd::atomic_bool m_clearContainers = false;
|
||||
|
||||
// When the thread is terminated, it will flag itself for deletion
|
||||
AZStd::atomic_bool m_deleteFlag = false;
|
||||
|
||||
// Keep track of the regions that have hit the size limit so we don't have to lock to check
|
||||
AZStd::map<AZStd::string, bool> m_hitSizeLimitMap;
|
||||
};
|
||||
|
||||
//! CpuProfiler will keep track of the registered threads, and
|
||||
//! forwards the request to profile a region to the appropriate thread. The user is able to request all
|
||||
//! cached regions, which are stored on a per thread frequency.
|
||||
class CpuProfilerImpl final
|
||||
: public AZ::Debug::Profiler
|
||||
, public CpuProfiler
|
||||
, public SystemTickBus::Handler
|
||||
{
|
||||
friend class CpuTimingLocalStorage;
|
||||
|
||||
public:
|
||||
AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}");
|
||||
AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0);
|
||||
|
||||
CpuProfilerImpl() = default;
|
||||
~CpuProfilerImpl() = default;
|
||||
|
||||
//! Registers the CpuProfilerImpl instance to the interface
|
||||
void Init();
|
||||
//! Unregisters the CpuProfilerImpl instance from the interface
|
||||
void Shutdown();
|
||||
|
||||
// SystemTickBus::Handler overrides
|
||||
// When fired, the profiler collects all profiling data from registered threads and updates
|
||||
// m_timeRegionMap so that the next frame has up-to-date profiling data.
|
||||
void OnSystemTick() final override;
|
||||
|
||||
//! AZ::Debug::Profiler overrides...
|
||||
void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override;
|
||||
void EndRegion(const AZ::Debug::Budget* budget) final override;
|
||||
|
||||
//! CpuProfiler overrides...
|
||||
const TimeRegionMap& GetTimeRegionMap() const final override;
|
||||
bool BeginContinuousCapture() final override;
|
||||
bool EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget) final override;
|
||||
bool IsContinuousCaptureInProgress() const final override;
|
||||
void SetProfilerEnabled(bool enabled) final override;
|
||||
bool IsProfilerEnabled() const final override;
|
||||
|
||||
private:
|
||||
static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps
|
||||
static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings.
|
||||
|
||||
// Lazily create and register the local thread data
|
||||
void RegisterThreadStorage();
|
||||
|
||||
// ThreadId -> ThreadTimeRegionMap
|
||||
// On the start of each frame, this map will be updated with the last frame's profiling data.
|
||||
TimeRegionMap m_timeRegionMap;
|
||||
|
||||
// Set of registered threads when created
|
||||
AZStd::vector<RHI::Ptr<CpuTimingLocalStorage>, AZ::OSStdAllocator> m_registeredThreads;
|
||||
AZStd::mutex m_threadRegisterMutex;
|
||||
|
||||
// Thread local storage, gets lazily allocated when a thread is created
|
||||
static thread_local CpuTimingLocalStorage* ms_threadLocalStorage;
|
||||
|
||||
// Enable/Disables the threads from profiling
|
||||
AZStd::atomic_bool m_enabled = false;
|
||||
|
||||
// This lock will only be contested when the CpuProfiler's Shutdown() method has been called
|
||||
AZStd::shared_mutex m_shutdownMutex;
|
||||
|
||||
bool m_initialized = false;
|
||||
|
||||
AZStd::mutex m_continuousCaptureEndingMutex;
|
||||
|
||||
AZStd::atomic_bool m_continuousCaptureInProgress;
|
||||
|
||||
// Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called.
|
||||
// Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality.
|
||||
AZStd::ring_buffer<TimeRegionMap> m_continuousCaptureData;
|
||||
};
|
||||
|
||||
// Intermediate class to serialize Cpu TimedRegion data.
|
||||
class CpuProfilingStatisticsSerializer
|
||||
{
|
||||
public:
|
||||
class CpuProfilingStatisticsSerializerEntry
|
||||
{
|
||||
public:
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry() = default;
|
||||
CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId);
|
||||
|
||||
Name m_groupName;
|
||||
Name m_regionName;
|
||||
uint16_t m_stackDepth;
|
||||
AZStd::sys_time_t m_startTick;
|
||||
AZStd::sys_time_t m_endTick;
|
||||
size_t m_threadId;
|
||||
};
|
||||
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializer() = default;
|
||||
CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData);
|
||||
|
||||
AZStd::vector<CpuProfilingStatisticsSerializerEntry> m_cpuProfilingStatisticsSerializerEntries;
|
||||
};
|
||||
}; // namespace RHI
|
||||
}; // namespace AZ
|
||||
@ -1,448 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <Atom/RHI/CpuProfilerImpl.h>
|
||||
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
#include <AzCore/std/smart_ptr/shared_ptr.h>
|
||||
|
||||
#include <AzCore/Debug/Timer.h>
|
||||
#include <AzCore/Statistics/StatisticalProfilerProxy.h>
|
||||
#include <Atom/RHI/RHIUtils.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace RHI
|
||||
{
|
||||
thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr;
|
||||
|
||||
// --- CpuProfiler ---
|
||||
|
||||
CpuProfiler* CpuProfiler::Get()
|
||||
{
|
||||
return Interface<CpuProfiler>::Get();
|
||||
}
|
||||
|
||||
// --- CachedTimeRegion ---
|
||||
|
||||
CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName)
|
||||
{
|
||||
m_groupRegionName = groupRegionName;
|
||||
}
|
||||
|
||||
CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick)
|
||||
{
|
||||
m_groupRegionName = groupRegionName;
|
||||
m_stackDepth = stackDepth;
|
||||
m_startTick = startTick;
|
||||
m_endTick = endTick;
|
||||
}
|
||||
|
||||
// --- GroupRegionName ---
|
||||
|
||||
CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) :
|
||||
m_groupName(group),
|
||||
m_regionName(region)
|
||||
{
|
||||
}
|
||||
|
||||
AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const
|
||||
{
|
||||
AZStd::size_t seed = 0;
|
||||
AZStd::hash_combine(seed, name.m_groupName);
|
||||
AZStd::hash_combine(seed, name.m_regionName);
|
||||
return seed;
|
||||
}
|
||||
|
||||
bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const
|
||||
{
|
||||
return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName);
|
||||
}
|
||||
|
||||
|
||||
// --- CpuProfilerImpl ---
|
||||
|
||||
void CpuProfilerImpl::Init()
|
||||
{
|
||||
Interface<AZ::Debug::Profiler>::Register(this);
|
||||
Interface<CpuProfiler>::Register(this);
|
||||
m_initialized = true;
|
||||
SystemTickBus::Handler::BusConnect();
|
||||
m_continuousCaptureData.set_capacity(10);
|
||||
|
||||
if (auto statsProfiler = AZ::Interface<AZ::Statistics::StatisticalProfilerProxy>::Get(); statsProfiler)
|
||||
{
|
||||
statsProfiler->ActivateProfiler(AZ_CRC_CE("RHI"), true);
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::Shutdown()
|
||||
{
|
||||
if (!m_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// When this call is made, no more thread profiling calls can be performed anymore
|
||||
Interface<CpuProfiler>::Unregister(this);
|
||||
Interface<AZ::Debug::Profiler>::Unregister(this);
|
||||
|
||||
// Wait for the remaining threads that might still be processing its profiling calls
|
||||
AZStd::unique_lock<AZStd::shared_mutex> shutdownLock(m_shutdownMutex);
|
||||
|
||||
m_enabled = false;
|
||||
|
||||
// Cleanup all TLS
|
||||
m_registeredThreads.clear();
|
||||
m_timeRegionMap.clear();
|
||||
m_initialized = false;
|
||||
m_continuousCaptureInProgress.store(false);
|
||||
m_continuousCaptureData.clear();
|
||||
SystemTickBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName)
|
||||
{
|
||||
// Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down.
|
||||
if (m_shutdownMutex.try_lock_shared())
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
// Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it
|
||||
RegisterThreadStorage();
|
||||
|
||||
// Push it to the stack
|
||||
CachedTimeRegion timeRegion({budget->Name(), eventName});
|
||||
ms_threadLocalStorage->RegionStackPushBack(timeRegion);
|
||||
}
|
||||
|
||||
m_shutdownMutex.unlock_shared();
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget)
|
||||
{
|
||||
// Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down.
|
||||
if (m_shutdownMutex.try_lock_shared())
|
||||
{
|
||||
// guard against enabling mid-marker
|
||||
if (m_enabled && ms_threadLocalStorage != nullptr)
|
||||
{
|
||||
ms_threadLocalStorage->RegionStackPopBack();
|
||||
}
|
||||
|
||||
m_shutdownMutex.unlock_shared();
|
||||
}
|
||||
}
|
||||
|
||||
const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const
|
||||
{
|
||||
return m_timeRegionMap;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::BeginContinuousCapture()
|
||||
{
|
||||
bool expected = false;
|
||||
if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true))
|
||||
{
|
||||
m_enabled = true;
|
||||
AZ_TracePrintf("Profiler", "Continuous capture started\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget)
|
||||
{
|
||||
if (!m_continuousCaptureInProgress.load())
|
||||
{
|
||||
AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_continuousCaptureEndingMutex.try_lock())
|
||||
{
|
||||
m_enabled = false;
|
||||
flushTarget = AZStd::move(m_continuousCaptureData);
|
||||
m_continuousCaptureData.clear();
|
||||
AZ_TracePrintf("Profiler", "Continuous capture ended\n");
|
||||
m_continuousCaptureInProgress.store(false);
|
||||
|
||||
m_continuousCaptureEndingMutex.unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::IsContinuousCaptureInProgress() const
|
||||
{
|
||||
return m_continuousCaptureInProgress.load();
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::SetProfilerEnabled(bool enabled)
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
|
||||
// Early out if the state is already the same or a continuous capture is in progress
|
||||
if (m_enabled == enabled || m_continuousCaptureInProgress.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the dirty flag in all the TLS to clear the caches
|
||||
if (enabled)
|
||||
{
|
||||
// Iterate through all the threads, and set the clearing flag
|
||||
for (auto& threadLocal : m_registeredThreads)
|
||||
{
|
||||
threadLocal->m_clearContainers = true;
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::IsProfilerEnabled() const
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::OnSystemTick()
|
||||
{
|
||||
if (!m_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock())
|
||||
{
|
||||
if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave)
|
||||
{
|
||||
const AZStd::size_t size = m_continuousCaptureData.size();
|
||||
m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2));
|
||||
}
|
||||
|
||||
m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap));
|
||||
m_timeRegionMap.clear();
|
||||
m_continuousCaptureEndingMutex.unlock();
|
||||
}
|
||||
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
|
||||
// Iterate through all the threads, and collect the thread's cached time regions
|
||||
TimeRegionMap newMap;
|
||||
for (auto& threadLocal : m_registeredThreads)
|
||||
{
|
||||
ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId];
|
||||
threadLocal->TryFlushCachedMap(threadMapEntry);
|
||||
}
|
||||
|
||||
// Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated
|
||||
AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const RHI::Ptr<CpuTimingLocalStorage>& thread)
|
||||
{
|
||||
return thread->m_deleteFlag.load();
|
||||
});
|
||||
|
||||
// Update our saved time regions to the last frame's collected data
|
||||
m_timeRegionMap = AZStd::move(newMap);
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::RegisterThreadStorage()
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
if (!ms_threadLocalStorage)
|
||||
{
|
||||
ms_threadLocalStorage = aznew CpuTimingLocalStorage();
|
||||
m_registeredThreads.emplace_back(ms_threadLocalStorage);
|
||||
}
|
||||
}
|
||||
|
||||
// --- CpuTimingLocalStorage ---
|
||||
|
||||
CpuTimingLocalStorage::CpuTimingLocalStorage()
|
||||
{
|
||||
m_executingThreadId = AZStd::this_thread::get_id();
|
||||
}
|
||||
|
||||
CpuTimingLocalStorage::~CpuTimingLocalStorage()
|
||||
{
|
||||
m_deleteFlag = true;
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion)
|
||||
{
|
||||
// If it was (re)enabled, clear the lists first
|
||||
if (m_clearContainers)
|
||||
{
|
||||
m_clearContainers = false;
|
||||
|
||||
m_stackLevel = 0;
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_timeRegionStack.clear();
|
||||
m_cachedTimeRegions.clear();
|
||||
}
|
||||
|
||||
timeRegion.m_stackDepth = static_cast<uint16_t>(m_stackLevel);
|
||||
|
||||
AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize.");
|
||||
m_timeRegionStack.push_back(timeRegion);
|
||||
|
||||
// Increment the stack
|
||||
m_stackLevel++;
|
||||
|
||||
// Set the starting time at the end, to avoid recording the minor overhead
|
||||
m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks();
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::RegionStackPopBack()
|
||||
{
|
||||
// Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers
|
||||
if (m_timeRegionStack.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the end timestamp here, to avoid the minor overhead
|
||||
const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks();
|
||||
|
||||
AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty.");
|
||||
CachedTimeRegion back = m_timeRegionStack.back();
|
||||
m_timeRegionStack.pop_back();
|
||||
|
||||
// Set the ending time
|
||||
back.m_endTick = endRegionTime;
|
||||
|
||||
// Decrement the stack
|
||||
m_stackLevel--;
|
||||
|
||||
// Add an entry to the cached region
|
||||
AddCachedRegion(back);
|
||||
}
|
||||
|
||||
// Gets called when region ends and all data is set
|
||||
void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached)
|
||||
{
|
||||
if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName])
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Add an entry to the cached region
|
||||
m_cachedTimeRegions.push_back(timeRegionCached);
|
||||
|
||||
// If the stack is empty, add it to the local cache map. Only gets called when the stack is empty
|
||||
// NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty
|
||||
// (i.e when the root region ended), this overhead won't affect any time regions.
|
||||
// The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this
|
||||
// case, the overhead of the profiled threads will be added to the main thread.
|
||||
if (m_timeRegionStack.empty())
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_cachedTimeRegionMutex);
|
||||
|
||||
// Add the cached regions to the map
|
||||
for (auto& cachedTimeRegion : m_cachedTimeRegions)
|
||||
{
|
||||
const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName;
|
||||
AZStd::vector<CachedTimeRegion>& regionVec = m_cachedTimeRegionMap[regionName];
|
||||
regionVec.push_back(cachedTimeRegion);
|
||||
if (regionVec.size() >= TimeRegionStackSize)
|
||||
{
|
||||
m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the cached regions
|
||||
m_cachedTimeRegions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap)
|
||||
{
|
||||
// Try to lock, if it's already in use (the cached regions in the array are being copied to the map)
|
||||
// it'll show up in the next iteration when the user requests it.
|
||||
if (m_cachedTimeRegionMutex.try_lock())
|
||||
{
|
||||
// Only flush cached time regions if there are entries available
|
||||
if (!m_cachedTimeRegionMap.empty())
|
||||
{
|
||||
cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap);
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_hitSizeLimitMap.clear();
|
||||
}
|
||||
m_cachedTimeRegionMutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// --- CpuProfilingStatisticsSerializer ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData)
|
||||
{
|
||||
// Create serializable entries
|
||||
for (const auto& timeRegionMap : continuousData)
|
||||
{
|
||||
for (const auto& [threadId, regionMap] : timeRegionMap)
|
||||
{
|
||||
for (const auto& [regionName, regionVec] : regionMap)
|
||||
{
|
||||
for (const auto& region : regionVec)
|
||||
{
|
||||
m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CpuProfilingStatisticsSerializer>()
|
||||
->Version(1)
|
||||
->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries)
|
||||
;
|
||||
}
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry::Reflect(context);
|
||||
}
|
||||
|
||||
// --- CpuProfilingStatisticsSerializerEntry ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(
|
||||
const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId)
|
||||
{
|
||||
m_groupName = cachedTimeRegion.m_groupRegionName.m_groupName;
|
||||
m_regionName = cachedTimeRegion.m_groupRegionName.m_regionName;
|
||||
m_stackDepth = cachedTimeRegion.m_stackDepth;
|
||||
m_startTick = cachedTimeRegion.m_startTick;
|
||||
m_endTick = cachedTimeRegion.m_endTick;
|
||||
m_threadId = AZStd::hash<AZStd::thread_id>{}(threadId);
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CpuProfilingStatisticsSerializerEntry>()
|
||||
->Version(1)
|
||||
->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName)
|
||||
->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName)
|
||||
->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth)
|
||||
->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick)
|
||||
->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick)
|
||||
->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId)
|
||||
;
|
||||
}
|
||||
}
|
||||
} // namespace RHI
|
||||
} // namespace AZ
|
||||
@ -1,231 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/IO/Path/Path.h>
|
||||
#include <AzCore/Math/Random.h>
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
//! Stores all the data associated with a row in the table.
|
||||
struct TableRow
|
||||
{
|
||||
template <typename T>
|
||||
struct TableRowCompareFunctor
|
||||
{
|
||||
TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){};
|
||||
|
||||
bool operator()(const TableRow* lhs, const TableRow* rhs)
|
||||
{
|
||||
return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer;
|
||||
}
|
||||
|
||||
T m_memberPointer;
|
||||
bool m_ascending;
|
||||
};
|
||||
|
||||
// Update running statistics with new region data
|
||||
void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId);
|
||||
|
||||
void ResetPerFrameStatistics();
|
||||
|
||||
// Get a string of all threads that this region executed in during the last frame
|
||||
AZStd::string GetExecutingThreadsLabel() const;
|
||||
|
||||
AZStd::string m_groupName;
|
||||
AZStd::string m_regionName;
|
||||
|
||||
// --- Per frame statistics ---
|
||||
|
||||
u64 m_invocationsLastFrame = 0;
|
||||
|
||||
// NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip.
|
||||
AZStd::set<size_t> m_executingThreads;
|
||||
|
||||
AZStd::sys_time_t m_lastFrameTotalTicks = 0;
|
||||
|
||||
// Maximum execution time of a region in the last frame.
|
||||
AZStd::sys_time_t m_maxTicks = 0;
|
||||
|
||||
// --- Aggregate statistics ---
|
||||
|
||||
u64 m_invocationsTotal = 0;
|
||||
|
||||
// Running average of Mean Time Per Call
|
||||
AZStd::sys_time_t m_runningAverageTicks = 0;
|
||||
};
|
||||
|
||||
//! ImGui widget for examining Atom CPU Profiling instrumentation.
|
||||
//! Offers both a statistical view (with sorting and searching capability) and a visualizer
|
||||
//! similar to RAD and other profiling tools.
|
||||
class ImGuiCpuProfiler
|
||||
: SystemTickBus::Handler
|
||||
{
|
||||
// Region Name -> statistical view row data
|
||||
using RegionRowMap = AZStd::map<AZStd::string, TableRow>;
|
||||
// Group Name -> RegionRowMap
|
||||
using GroupRegionMap = AZStd::map<AZStd::string, RegionRowMap>;
|
||||
|
||||
using TimeRegion = AZ::RHI::CachedTimeRegion;
|
||||
using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName;
|
||||
|
||||
public:
|
||||
struct CpuTimingEntry
|
||||
{
|
||||
const AZStd::string& m_name;
|
||||
double m_executeDuration;
|
||||
};
|
||||
|
||||
ImGuiCpuProfiler() = default;
|
||||
~ImGuiCpuProfiler() = default;
|
||||
|
||||
//! Draws the overall CPU profiling window, defaults to the statistical view
|
||||
void Draw(bool& keepDrawing);
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 35.0;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
static constexpr float MediumFrameTimeLimit = 16.6; // 60 fps
|
||||
static constexpr float HighFrameTimeLimit = 33.3; // 30 fps
|
||||
|
||||
//! Draws the statistical view of the CPU profiling data.
|
||||
void DrawStatisticsView();
|
||||
|
||||
//! Callback invoked when the "Load File" button is pressed in the file picker.
|
||||
void LoadFile();
|
||||
|
||||
//! Draws the file picker window.
|
||||
void DrawFilePicker();
|
||||
|
||||
//! Draws the CPU profiling visualizer.
|
||||
void DrawVisualizer();
|
||||
|
||||
// Draw the shared header between the two windows.
|
||||
void DrawCommonHeader();
|
||||
|
||||
// Draw the region statistics table in the order specified by the pointers in m_tableData.
|
||||
void DrawTable();
|
||||
|
||||
// Sort the table by a given column, rearranges the pointers in m_tableData.
|
||||
void SortTable(ImGuiTableSortSpecs* sortSpecs);
|
||||
|
||||
// gather the latest timing statistics
|
||||
void CacheCpuTimingStatistics();
|
||||
|
||||
// Get the profiling data from the last frame, only called when the profiler is not paused.
|
||||
void CollectFrameData();
|
||||
|
||||
// Cull old data from internal storage, only called when profiler is not paused.
|
||||
void CullFrameData();
|
||||
|
||||
// Draws a single block onto the timeline into the specified row
|
||||
void DrawBlock(const TimeRegion& block, u64 targetRow);
|
||||
|
||||
// Draw horizontal lines between threads in the timeline
|
||||
void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth);
|
||||
|
||||
// Draw the "Thread XXXXX" label onto the viewport
|
||||
void DrawThreadLabel(u64 baseRow, size_t threadId);
|
||||
|
||||
// Draw the vertical lines separating frames in the timeline
|
||||
void DrawFrameBoundaries();
|
||||
|
||||
// Draw the ruler with frame time labels
|
||||
void DrawRuler();
|
||||
|
||||
// Draw the frame time histogram
|
||||
void DrawFrameTimeHistogram();
|
||||
|
||||
// Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling
|
||||
float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const;
|
||||
|
||||
AZStd::sys_time_t GetViewportTickWidth() const;
|
||||
|
||||
// Gets the color for a block using the GroupRegionName as a key into the cache.
|
||||
// Generates a random ImU32 if the block does not yet have a color.
|
||||
ImU32 GetBlockColor(const TimeRegion& block);
|
||||
|
||||
// System tick bus overrides
|
||||
virtual void OnSystemTick() override;
|
||||
|
||||
// --- Visualizer Members ---
|
||||
|
||||
int m_framesToCollect = DefaultFramesToCollect;
|
||||
|
||||
// Tally of the number of saved profiling events so far
|
||||
u64 m_savedRegionCount = 0;
|
||||
|
||||
// Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects
|
||||
AZStd::sys_time_t m_viewportStartTick;
|
||||
AZStd::sys_time_t m_viewportEndTick;
|
||||
|
||||
// Map to store each thread's TimeRegions, individual vectors are sorted by start tick
|
||||
// note: we use size_t as a proxy for thread_id because native_thread_id_type differs differs from
|
||||
// platform to platform, which causes problems when deserializing saved captures.
|
||||
AZStd::unordered_map<size_t, AZStd::vector<TimeRegion>> m_savedData;
|
||||
|
||||
// Region color cache
|
||||
AZStd::unordered_map<GroupRegionName, ImVec4, RHI::CachedTimeRegion::GroupRegionName::Hash> m_regionColorMap;
|
||||
|
||||
// Tracks the frame boundaries
|
||||
AZStd::vector<AZStd::sys_time_t> m_frameEndTicks = { INT64_MIN };
|
||||
|
||||
// Filter for highlighting regions on the visualizer
|
||||
ImGuiTextFilter m_visualizerHighlightFilter;
|
||||
|
||||
// --- Tabular view members ---
|
||||
|
||||
// ImGui filter used to filter TimedRegions.
|
||||
ImGuiTextFilter m_timedRegionFilter;
|
||||
|
||||
// Saves statistical view data organized by group name -> region name -> row data
|
||||
GroupRegionMap m_groupRegionMap;
|
||||
|
||||
// Saves pointers to objects in m_groupRegionMap, order reflects table ordering.
|
||||
// Non-owning, will be cleared when m_groupRegionMap is cleared.
|
||||
AZStd::vector<TableRow*> m_tableData;
|
||||
|
||||
// Pause cpu profiling. The profiler will show the statistics of the last frame before pause.
|
||||
bool m_paused = false;
|
||||
|
||||
// Export the profiling data from a single frame to a local file.
|
||||
bool m_captureToFile = false;
|
||||
|
||||
// Toggle between the normal statistical view and the visual profiling view.
|
||||
bool m_enableVisualizer = false;
|
||||
|
||||
// Last captured CPU timing statistics
|
||||
AZStd::vector<CpuTimingEntry> m_cpuTimingStatisticsWhenPause;
|
||||
AZStd::sys_time_t m_frameToFrameTime{};
|
||||
|
||||
AZStd::string m_lastCapturedFilePath;
|
||||
|
||||
bool m_showFilePicker = false;
|
||||
|
||||
// Cached file paths to previous traces on disk, sorted with the most recent trace at the front.
|
||||
AZStd::vector<IO::Path> m_cachedCapturePaths;
|
||||
|
||||
// Index into the file picker, used to determine which file to load when "Load File" is pressed.
|
||||
int m_currentFileIndex = 0;
|
||||
|
||||
|
||||
// --- Loading capture state ---
|
||||
AZStd::unordered_set<AZStd::string> m_deserializedStringPool;
|
||||
AZStd::unordered_set<RHI::CachedTimeRegion::GroupRegionName, RHI::CachedTimeRegion::GroupRegionName::Hash> m_deserializedGroupRegionNamePool;
|
||||
};
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
#include "ImGuiCpuProfiler.inl"
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue