diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RPIUtils.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RPIUtils.h index 70754777c7..546089ab1e 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RPIUtils.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RPIUtils.h @@ -16,6 +16,8 @@ #include #include +#include + namespace AZ { namespace RPI @@ -57,7 +59,22 @@ namespace AZ //! Same as above. Provided as a convenience when all arguments of the 'numthreads' attributes should be assigned to RHI::DispatchDirect::m_threadsPerGroup* variables. AZ::Outcome GetComputeShaderNumThreads(const Data::Asset& shaderAsset, RHI::DispatchDirect& dispatchDirect); - + + //! Get single image pixel value for specified mip and slice + template + T GetSubImagePixelValue(const AZ::Data::Asset& imageAsset, uint32_t x, uint32_t y, uint32_t componentIndex = 0, uint32_t mip = 0, uint32_t slice = 0); + + //! Retrieve a region of image pixel values (float) for specified mip and slice + //! NOTE: The topLeft coordinate is inclusive, whereas the bottomRight is exclusive + bool GetSubImagePixelValues(const AZ::Data::Asset& imageAsset, AZStd::pair topLeft, AZStd::pair bottomRight, AZStd::span outValues, uint32_t componentIndex = 0, uint32_t mip = 0, uint32_t slice = 0); + + //! Retrieve a region of image pixel values (uint) for specified mip and slice + //! NOTE: The topLeft coordinate is inclusive, whereas the bottomRight is exclusive + bool GetSubImagePixelValues(const AZ::Data::Asset& imageAsset, AZStd::pair topLeft, AZStd::pair bottomRight, AZStd::span outValues, uint32_t componentIndex = 0, uint32_t mip = 0, uint32_t slice = 0); + + //! Retrieve a region of image pixel values (int) for specified mip and slice + //! NOTE: The topLeft coordinate is inclusive, whereas the bottomRight is exclusive + bool GetSubImagePixelValues(const AZ::Data::Asset& imageAsset, AZStd::pair topLeft, AZStd::pair bottomRight, AZStd::span outValues, uint32_t componentIndex = 0, uint32_t mip = 0, uint32_t slice = 0); } // namespace RPI } // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp index 7285273c3e..2dd2885dff 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp @@ -20,6 +20,202 @@ namespace AZ { namespace RPI { + namespace Internal + { + // The original implementation was from cryhalf's CryConvertFloatToHalf and CryConvertHalfToFloat function + // Will be replaced with centralized half float API + struct SHalf + { + explicit SHalf(float floatValue) + { + AZ::u32 Result; + + AZ::u32 intValue = ((AZ::u32*)(&floatValue))[0]; + AZ::u32 Sign = (intValue & 0x80000000U) >> 16U; + intValue = intValue & 0x7FFFFFFFU; + + if (intValue > 0x47FFEFFFU) + { + // The number is too large to be represented as a half. Saturate to infinity. + Result = 0x7FFFU; + } + else + { + if (intValue < 0x38800000U) + { + // The number is too small to be represented as a normalized half. + // Convert it to a denormalized value. + AZ::u32 Shift = 113U - (intValue >> 23U); + intValue = (0x800000U | (intValue & 0x7FFFFFU)) >> Shift; + } + else + { + // Rebias the exponent to represent the value as a normalized half. + intValue += 0xC8000000U; + } + + Result = ((intValue + 0x0FFFU + ((intValue >> 13U) & 1U)) >> 13U) & 0x7FFFU; + } + h = static_cast(Result | Sign); + } + + operator float() const + { + AZ::u32 Mantissa; + AZ::u32 Exponent; + AZ::u32 Result; + + Mantissa = h & 0x03FF; + + if ((h & 0x7C00) != 0) // The value is normalized + { + Exponent = ((h >> 10) & 0x1F); + } + else if (Mantissa != 0) // The value is denormalized + { + // Normalize the value in the resulting float + Exponent = 1; + + do + { + Exponent--; + Mantissa <<= 1; + } while ((Mantissa & 0x0400) == 0); + + Mantissa &= 0x03FF; + } + else // The value is zero + { + Exponent = static_cast(-112); + } + + Result = ((h & 0x8000) << 16) | // Sign + ((Exponent + 112) << 23) | // Exponent + (Mantissa << 13); // Mantissa + + return *(float*)&Result; + } + + private: + AZ::u16 h; + }; + + float ScaleValue(float value, float origMin, float origMax, float scaledMin, float scaledMax) + { + return ((value - origMin) / (origMax - origMin)) * (scaledMax - scaledMin) + scaledMin; + } + + float RetrieveFloatValue(const AZ::u8* mem, size_t index, AZ::RHI::Format format) + { + switch (format) + { + case AZ::RHI::Format::R8_UNORM: + case AZ::RHI::Format::A8_UNORM: + { + return mem[index] / static_cast(std::numeric_limits::max()); + } + case AZ::RHI::Format::R8_SNORM: + { + // Scale the value from AZ::s8 min/max to -1 to 1 + // We need to treat -128 and -127 the same, so that we get a symmetric + // range of -127 to 127 with complementary scaled values of -1 to 1 + auto actualMem = reinterpret_cast(mem); + AZ::s8 signedMax = std::numeric_limits::max(); + AZ::s8 signedMin = aznumeric_cast(-signedMax); + return ScaleValue(AZStd::max(actualMem[index], signedMin), signedMin, signedMax, -1.0f, 1.0f); + } + case AZ::RHI::Format::D16_UNORM: + case AZ::RHI::Format::R16_UNORM: + { + return mem[index] / static_cast(std::numeric_limits::max()); + } + case AZ::RHI::Format::R16_SNORM: + { + // Scale the value from AZ::s16 min/max to -1 to 1 + // We need to treat -32768 and -32767 the same, so that we get a symmetric + // range of -32767 to 32767 with complementary scaled values of -1 to 1 + auto actualMem = reinterpret_cast(mem); + AZ::s16 signedMax = std::numeric_limits::max(); + AZ::s16 signedMin = aznumeric_cast(-signedMax); + return ScaleValue(AZStd::max(actualMem[index], signedMin), signedMin, signedMax, -1.0f, 1.0f); + } + case AZ::RHI::Format::R16_FLOAT: + { + auto actualMem = reinterpret_cast(mem); + return SHalf(actualMem[index]); + } + case AZ::RHI::Format::D32_FLOAT: + case AZ::RHI::Format::R32_FLOAT: + { + auto actualMem = reinterpret_cast(mem); + return actualMem[index]; + } + default: + AZ_Assert(false, "Unsupported pixel format"); + return 0.0f; + } + } + + AZ::u32 RetrieveUintValue(const AZ::u8* mem, size_t index, AZ::RHI::Format format) + { + switch (format) + { + case AZ::RHI::Format::R8_UINT: + { + return mem[index] / static_cast(std::numeric_limits::max()); + } + case AZ::RHI::Format::R16_UINT: + { + auto actualMem = reinterpret_cast(mem); + return actualMem[index] / static_cast(std::numeric_limits::max()); + } + case AZ::RHI::Format::R32_UINT: + { + auto actualMem = reinterpret_cast(mem); + return actualMem[index]; + } + default: + AZ_Assert(false, "Unsupported pixel format"); + return 0; + } + } + + AZ::s32 RetrieveIntValue(const AZ::u8* mem, size_t index, AZ::RHI::Format format) + { + switch (format) + { + case AZ::RHI::Format::R8_SINT: + { + return mem[index] / static_cast(std::numeric_limits::max()); + } + case AZ::RHI::Format::R16_SINT: + { + auto actualMem = reinterpret_cast(mem); + return actualMem[index] / static_cast(std::numeric_limits::max()); + } + case AZ::RHI::Format::R32_SINT: + { + auto actualMem = reinterpret_cast(mem); + return actualMem[index]; + } + default: + AZ_Assert(false, "Unsupported pixel format"); + return 0; + } + } + + template + T GetSubImagePixelValueInternal(const AZ::Data::Asset& imageAsset, uint32_t x, uint32_t y, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + AZStd::array values{ aznumeric_cast(0) }; + + auto topLeft = AZStd::make_pair(x, y); + auto bottomRight = AZStd::make_pair(x + 1, y + 1); + GetSubImagePixelValues(imageAsset, topLeft, bottomRight, AZStd::span(values), componentIndex, mip, slice); + + return values[0]; + } + } Data::AssetId GetShaderAssetId(const AZStd::string& shaderFilePath, bool isCritical) { @@ -222,5 +418,128 @@ namespace AZ { return GetComputeShaderNumThreads(shaderAsset, &dispatchDirect.m_threadsPerGroupX, &dispatchDirect.m_threadsPerGroupY, &dispatchDirect.m_threadsPerGroupZ); } + + template<> + float GetSubImagePixelValue(const AZ::Data::Asset& imageAsset, uint32_t x, uint32_t y, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + return Internal::GetSubImagePixelValueInternal(imageAsset, x, y, componentIndex, mip, slice); + } + + template<> + AZ::u32 GetSubImagePixelValue(const AZ::Data::Asset& imageAsset, uint32_t x, uint32_t y, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + return Internal::GetSubImagePixelValueInternal(imageAsset, x, y, componentIndex, mip, slice); + } + + template<> + AZ::s32 GetSubImagePixelValue(const AZ::Data::Asset& imageAsset, uint32_t x, uint32_t y, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + return Internal::GetSubImagePixelValueInternal(imageAsset, x, y, componentIndex, mip, slice); + } + + bool GetSubImagePixelValues(const AZ::Data::Asset& imageAsset, AZStd::pair topLeft, AZStd::pair bottomRight, AZStd::span outValues, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + // TODO: Use the component index + (void)componentIndex; + + if (!imageAsset.IsReady()) + { + return false; + } + + auto imageData = imageAsset->GetSubImageData(mip, slice); + if (imageData.empty()) + { + return false; + } + + const AZ::RHI::ImageDescriptor imageDescriptor = imageAsset->GetImageDescriptor(); + auto width = imageDescriptor.m_size.m_width; + const uint32_t pixelSize = AZ::RHI::GetFormatSize(imageDescriptor.m_format); + + size_t outValuesIndex = 0; + for (uint32_t y = topLeft.second; y < bottomRight.second; ++y) + { + for (uint32_t x = topLeft.first; x < bottomRight.first; ++x) + { + size_t imageDataIndex = (y * width + x) * pixelSize; + + auto& outValue = outValues[outValuesIndex++]; + outValue = Internal::RetrieveFloatValue(imageData.data(), imageDataIndex, imageDescriptor.m_format); + } + } + + return true; + } + + bool GetSubImagePixelValues(const AZ::Data::Asset& imageAsset, AZStd::pair topLeft, AZStd::pair bottomRight, AZStd::span outValues, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + // TODO: Use the component index + (void)componentIndex; + + if (!imageAsset.IsReady()) + { + return false; + } + + auto imageData = imageAsset->GetSubImageData(mip, slice); + if (imageData.empty()) + { + return false; + } + + const AZ::RHI::ImageDescriptor imageDescriptor = imageAsset->GetImageDescriptor(); + auto width = imageDescriptor.m_size.m_width; + const uint32_t pixelSize = AZ::RHI::GetFormatSize(imageDescriptor.m_format); + + size_t outValuesIndex = 0; + for (uint32_t y = topLeft.second; y < bottomRight.second; ++y) + { + for (uint32_t x = topLeft.first; x < bottomRight.first; ++x) + { + size_t imageDataIndex = (y * width + x) * pixelSize; + + auto& outValue = outValues[outValuesIndex++]; + outValue = Internal::RetrieveUintValue(imageData.data(), imageDataIndex, imageDescriptor.m_format); + } + } + + return true; + } + + bool GetSubImagePixelValues(const AZ::Data::Asset& imageAsset, AZStd::pair topLeft, AZStd::pair bottomRight, AZStd::span outValues, uint32_t componentIndex, uint32_t mip, uint32_t slice) + { + // TODO: Use the component index + (void)componentIndex; + + if (!imageAsset.IsReady()) + { + return false; + } + + auto imageData = imageAsset->GetSubImageData(mip, slice); + if (imageData.empty()) + { + return false; + } + + const AZ::RHI::ImageDescriptor imageDescriptor = imageAsset->GetImageDescriptor(); + auto width = imageDescriptor.m_size.m_width; + const uint32_t pixelSize = AZ::RHI::GetFormatSize(imageDescriptor.m_format); + + size_t outValuesIndex = 0; + for (uint32_t y = topLeft.second; y < bottomRight.second; ++y) + { + for (uint32_t x = topLeft.first; x < bottomRight.first; ++x) + { + size_t imageDataIndex = (y * width + x) * pixelSize; + + auto& outValue = outValues[outValuesIndex++]; + outValue = Internal::RetrieveIntValue(imageData.data(), imageDataIndex, imageDescriptor.m_format); + } + } + + return true; + } } } diff --git a/Gems/Atom/RPI/Code/Tests/Image/StreamingImageTests.cpp b/Gems/Atom/RPI/Code/Tests/Image/StreamingImageTests.cpp index cb8d903673..63ae252105 100644 --- a/Gems/Atom/RPI/Code/Tests/Image/StreamingImageTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Image/StreamingImageTests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -433,12 +434,12 @@ namespace UnitTest return poolAsset; } - AZ::Data::Asset BuildTestImage() + AZ::Data::Asset BuildTestImage(AZ::RHI::Format format = AZ::RHI::Format::R8G8B8A8_UNORM) { using namespace AZ; const uint32_t arraySize = 2; - const uint32_t pixelSize = 4; + const uint32_t pixelSize = RHI::GetFormatSize(format); const uint32_t mipCountHead = 1; const uint32_t mipCountMiddle = 2; const uint32_t mipCountTail = 3; @@ -453,7 +454,7 @@ namespace UnitTest RPI::StreamingImageAssetCreator assetCreator; assetCreator.Begin(Data::AssetId(Uuid::CreateRandom())); - RHI::ImageDescriptor imageDesc = RHI::ImageDescriptor::Create2DArray(RHI::ImageBindFlags::ShaderRead, imageWidth, imageHeight, arraySize, RHI::Format::R8G8B8A8_UNORM); + RHI::ImageDescriptor imageDesc = RHI::ImageDescriptor::Create2DArray(RHI::ImageBindFlags::ShaderRead, imageWidth, imageHeight, arraySize, format); imageDesc.m_mipLevels = static_cast(mipCountTotal); assetCreator.SetImageDescriptor(imageDesc); @@ -726,4 +727,40 @@ namespace UnitTest RPI::ImageSystemInterface::Get()->Update(); } + + TEST_F(StreamingImageTests, GetSubImagePixelValues) + { + using namespace AZ; + + Data::Asset imageAsset = BuildTestImage(AZ::RHI::Format::R8_UNORM); + + auto streamingImageAsset = imageAsset.Get(); + EXPECT_NE(streamingImageAsset, nullptr); + + // Validate retrieving one pixel at a time + auto size = streamingImageAsset->GetImageDescriptor().m_size; + for (uint32_t y = 0; y < size.m_height; ++y) + { + for (uint32_t x = 0; x < size.m_width; ++x) + { + auto pixelDataValue = RPI::GetSubImagePixelValue(imageAsset, x, y); + auto pixelExpectedValue = static_cast(y * size.m_width + x) / static_cast(std::numeric_limits::max()); + + EXPECT_NEAR(pixelDataValue, pixelExpectedValue, Constants::Tolerance); + } + } + + // Validate retrieving a region of pixels + AZStd::vector pixelValues(size.m_width * size.m_height); + auto topLeft = AZStd::make_pair(0, 0); + auto bottomRight = AZStd::make_pair(size.m_width, size.m_height); + RPI::GetSubImagePixelValues(imageAsset, topLeft, bottomRight, pixelValues); + for (uint32_t index = 0; index < pixelValues.size(); ++index) + { + auto pixelDataValue = pixelValues[index]; + auto pixelExpectedValue = static_cast(index) / static_cast(std::numeric_limits::max()); + + EXPECT_NEAR(pixelDataValue, pixelExpectedValue, Constants::Tolerance); + } + } }