You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
526 lines
22 KiB
C++
526 lines
22 KiB
C++
/*
|
|
* 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/RPI.Public/Image/ImageSystemInterface.h>
|
|
#include <Atom/RPI.Public/Image/StreamingImage.h>
|
|
#include <Atom/RPI.Public/Image/StreamingImagePool.h>
|
|
#include <Atom/RPI.Public/Image/StreamingImageController.h>
|
|
|
|
#include <Atom/RPI.Reflect/Image/ImageMipChainAssetCreator.h>
|
|
#include <Atom/RPI.Reflect/Image/StreamingImageAssetCreator.h>
|
|
|
|
#include <Atom/RHI/Factory.h>
|
|
|
|
#include <AzCore/Debug/EventTrace.h>
|
|
#include <AtomCore/Instance/InstanceDatabase.h>
|
|
|
|
// Enable this define to debug output streaming image initialization and expanding process.
|
|
//#define AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
|
|
namespace AZ
|
|
{
|
|
namespace RPI
|
|
{
|
|
Data::Instance<StreamingImage> StreamingImage::FindOrCreate(const Data::Asset<StreamingImageAsset>& streamingImageAsset)
|
|
{
|
|
return Data::InstanceDatabase<StreamingImage>::Instance().FindOrCreate(
|
|
Data::InstanceId::CreateFromAssetId(streamingImageAsset.GetId()),
|
|
streamingImageAsset);
|
|
}
|
|
|
|
Data::Instance<StreamingImage> StreamingImage::CreateFromCpuData(
|
|
const StreamingImagePool& streamingImagePool,
|
|
RHI::ImageDimension imageDimension,
|
|
RHI::Size imageSize,
|
|
RHI::Format imageFormat,
|
|
const void* imageData,
|
|
size_t imageDataSize,
|
|
Uuid id)
|
|
{
|
|
Data::Instance<StreamingImage> existingImage = Data::InstanceDatabase<StreamingImage>::Instance().Find(Data::InstanceId::CreateFromAssetId(id));
|
|
AZ_Error("StreamingImage", !existingImage, "StreamingImage::CreateFromCpuData found an existing entry in the instance database for the provided id.");
|
|
|
|
RHI::ImageDescriptor imageDescriptor;
|
|
imageDescriptor.m_bindFlags = RHI::ImageBindFlags::ShaderRead;
|
|
imageDescriptor.m_dimension = imageDimension;
|
|
imageDescriptor.m_size = imageSize;
|
|
imageDescriptor.m_format = imageFormat;
|
|
|
|
const RHI::ImageSubresourceLayout imageSubresourceLayout = RHI::GetImageSubresourceLayout(imageDescriptor, RHI::ImageSubresource{});
|
|
|
|
const size_t expectedImageDataSize = imageSubresourceLayout.m_bytesPerImage * imageDescriptor.m_size.m_depth;
|
|
if (expectedImageDataSize != imageDataSize)
|
|
{
|
|
AZ_Error("StreamingImage", false, "StreamingImage::CreateFromCpuData expected '%d' bytes of image data, but got '%d' instead.", expectedImageDataSize, imageDataSize);
|
|
return nullptr;
|
|
}
|
|
|
|
Data::Asset<ImageMipChainAsset> mipChainAsset;
|
|
|
|
// Construct the mip chain asset.
|
|
{
|
|
ImageMipChainAssetCreator assetCreator;
|
|
assetCreator.Begin(Uuid::CreateRandom(), 1, 1);
|
|
assetCreator.BeginMip(imageSubresourceLayout);
|
|
assetCreator.AddSubImage(imageData, expectedImageDataSize);
|
|
assetCreator.EndMip();
|
|
if (!assetCreator.End(mipChainAsset))
|
|
{
|
|
AZ_Error("StreamingImage", false, "Failed to initialize mip chain asset");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
Data::Asset<StreamingImageAsset> streamingImageAsset;
|
|
|
|
// Construct the streaming image asset.
|
|
{
|
|
StreamingImageAssetCreator assetCreator;
|
|
assetCreator.Begin(id);
|
|
assetCreator.SetImageDescriptor(imageDescriptor);
|
|
assetCreator.AddMipChainAsset(*mipChainAsset.Get());
|
|
assetCreator.SetFlags(StreamingImageFlags::NotStreamable);
|
|
assetCreator.SetPoolAssetId(streamingImagePool.GetAssetId());
|
|
if (!assetCreator.End(streamingImageAsset))
|
|
{
|
|
AZ_Error("StreamingImage", false, "Failed to initialize streaming image asset");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return StreamingImage::FindOrCreate(streamingImageAsset);
|
|
}
|
|
|
|
Data::Instance<StreamingImage> StreamingImage::CreateInternal(StreamingImageAsset& streamingImageAsset)
|
|
{
|
|
Data::Instance<StreamingImage> streamingImage = aznew StreamingImage();
|
|
const RHI::ResultCode resultCode = streamingImage->Init(streamingImageAsset);
|
|
|
|
if (resultCode == RHI::ResultCode::Success)
|
|
{
|
|
return streamingImage;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
RHI::ResultCode StreamingImage::Init(StreamingImageAsset& imageAsset)
|
|
{
|
|
AZ_TRACE_METHOD();
|
|
|
|
Data::Instance<StreamingImagePool> pool;
|
|
if (imageAsset.GetPoolAssetId().IsValid())
|
|
{
|
|
Data::Asset<RPI::StreamingImagePoolAsset> poolAsset(imageAsset.GetPoolAssetId(), AZ::AzTypeInfo<RPI::StreamingImagePoolAsset>::Uuid());
|
|
pool = StreamingImagePool::FindOrCreate(poolAsset);
|
|
}
|
|
else
|
|
{
|
|
pool = ImageSystemInterface::Get()->GetStreamingPool();
|
|
}
|
|
|
|
if (!pool)
|
|
{
|
|
AZ_Error("StreamingImage", false, "Failed to acquire the streaming image pool instance.");
|
|
return RHI::ResultCode::Fail;
|
|
}
|
|
|
|
// Cache off the RHI streaming image pool instance.
|
|
RHI::StreamingImagePool* rhiPool = pool->GetRHIPool();
|
|
|
|
/**
|
|
* NOTE: The tail mip-chain is required to exist as a dependency of this asset. This allows
|
|
* the image to initialize with well-defined content.
|
|
*/
|
|
const uint16_t mipChainTailIndex = static_cast<uint16_t>(imageAsset.GetMipChainCount() - 1);
|
|
|
|
RHI::ResultCode resultCode = RHI::ResultCode::Success;
|
|
|
|
const ImageMipChainAsset& mipChainTailAsset = imageAsset.GetTailMipChain();
|
|
|
|
#ifdef AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
m_image->SetName(Name(imageAsset.GetHint().c_str()));
|
|
AZ_TracePrintf("StreamingImage", "Init image [%s]\n", m_image->GetName().data());
|
|
#endif
|
|
|
|
{
|
|
RHI::StreamingImageInitRequest initRequest;
|
|
initRequest.m_image = GetRHIImage();
|
|
initRequest.m_descriptor = imageAsset.GetImageDescriptor();
|
|
initRequest.m_tailMipSlices = mipChainTailAsset.GetMipSlices();
|
|
|
|
// NOTE: Initialization can fail due to out-of-memory errors. Need to handle it at runtime.
|
|
resultCode = rhiPool->InitImage(initRequest);
|
|
}
|
|
|
|
if (resultCode == RHI::ResultCode::Success)
|
|
{
|
|
m_imageView = m_image->GetImageView(imageAsset.GetImageViewDescriptor());
|
|
if(!m_imageView.get())
|
|
{
|
|
AZ_Error("Image", false, "Failed to initialize RHI image view. This is not a recoverable error and is likely a bug.");
|
|
return RHI::ResultCode::Fail;
|
|
}
|
|
|
|
// Build a local set of mip chain asset handles.
|
|
for (size_t mipChainIndex = 0; mipChainIndex < imageAsset.GetMipChainCount(); ++mipChainIndex)
|
|
{
|
|
const Data::AssetId& assetId = imageAsset.GetMipChainAsset(mipChainIndex).GetId();
|
|
|
|
// We want to store off the id, not the AssetData instance. This simplifies the fetch / evict logic which
|
|
// can do more strict assertions.
|
|
m_mipChains.push_back(Data::Asset<ImageMipChainAsset>(assetId, azrtti_typeid<ImageMipChainAsset>()));
|
|
}
|
|
|
|
// Initialize the streaming state to have the tail mip active and ready.
|
|
m_state.m_residencyTarget = mipChainTailIndex;
|
|
m_state.m_streamingTarget = mipChainTailIndex;
|
|
|
|
// Setup masks for tail mip chain
|
|
const uint16_t mipChainBit = static_cast<uint16_t>(1 << mipChainTailIndex);
|
|
m_state.m_maskActive |= mipChainBit;
|
|
m_state.m_maskEvictable &= ~mipChainBit;
|
|
m_state.m_maskReady |= mipChainBit;
|
|
|
|
// Take references on dependent assets
|
|
m_imageAsset = { &imageAsset, AZ::Data::AssetLoadBehavior::PreLoad };
|
|
m_rhiPool = rhiPool;
|
|
m_pool = pool;
|
|
m_pool->AttachImage(this);
|
|
|
|
#if defined (AZ_RPI_STREAMING_IMAGE_HOT_RELOADING)
|
|
BusConnect(imageAsset.GetId());
|
|
#endif
|
|
return RHI::ResultCode::Success;
|
|
}
|
|
|
|
AZ_Warning("StreamingImagePool", false, "Failed to initialize RHI::Image on RHI::StreamingImagePool.");
|
|
return resultCode;
|
|
}
|
|
|
|
void StreamingImage::Shutdown()
|
|
{
|
|
if (IsInitialized())
|
|
{
|
|
#if defined (AZ_RPI_STREAMING_IMAGE_HOT_RELOADING)
|
|
Data::AssetBus::MultiHandler::BusDisconnect(GetAssetId());
|
|
#endif
|
|
|
|
if(m_pool)
|
|
{
|
|
m_pool->DetachImage(this);
|
|
m_pool = nullptr;
|
|
}
|
|
|
|
m_rhiPool = nullptr;
|
|
|
|
GetRHIImage()->Shutdown();
|
|
|
|
// Evict all active mip chains
|
|
for (size_t mipChainIndex = 0; mipChainIndex < m_mipChains.size(); ++mipChainIndex)
|
|
{
|
|
EvictMipChainAsset(mipChainIndex);
|
|
}
|
|
|
|
m_mipChains.clear();
|
|
m_state = {};
|
|
}
|
|
}
|
|
|
|
StreamingImage::~StreamingImage()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
void StreamingImage::SetTargetMip(uint16_t targetMipLevel)
|
|
{
|
|
if (m_streamingController)
|
|
{
|
|
m_streamingController->OnSetTargetMip(this, targetMipLevel);
|
|
}
|
|
}
|
|
|
|
uint16_t StreamingImage::GetResidentMipLevel()
|
|
{
|
|
return static_cast<uint16_t>(m_image->GetResidentMipLevel());
|
|
}
|
|
|
|
RHI::ResultCode StreamingImage::TrimToMipChainLevel(size_t mipChainIndex)
|
|
{
|
|
AZ_Assert(mipChainIndex < m_mipChains.size(), "Exceeded number of mip chains.");
|
|
|
|
const size_t mipChainBegin = m_state.m_streamingTarget;
|
|
const size_t mipChainEnd = mipChainIndex;
|
|
|
|
RHI::ResultCode resultCode = RHI::ResultCode::Success;
|
|
|
|
// We only evict if the current target is higher detail than our requested target.
|
|
if (mipChainBegin < mipChainEnd)
|
|
{
|
|
const uint32_t mipLevel = static_cast<uint32_t>(m_imageAsset->GetMipLevel(mipChainEnd));
|
|
resultCode = m_rhiPool->TrimImage(*m_image.get(), mipLevel);
|
|
|
|
// Start from the most detailed chain, evict all in-flight or loaded assets.
|
|
// Note: this should only happened after TrimImage which all the possible backend asset data referencing were removed
|
|
for (size_t chainIdx = mipChainBegin; chainIdx < mipChainEnd; ++chainIdx)
|
|
{
|
|
EvictMipChainAsset(chainIdx);
|
|
}
|
|
|
|
// Reset tracked state to match the new target.
|
|
m_state.m_residencyTarget = static_cast<uint16_t>(mipChainEnd);
|
|
m_state.m_streamingTarget = static_cast<uint16_t>(mipChainEnd);
|
|
}
|
|
|
|
return resultCode;
|
|
}
|
|
|
|
void StreamingImage::QueueExpandToMipChainLevel(size_t mipChainIndex)
|
|
{
|
|
AZ_Assert(IsStreamable(), "Only streamable StreamingImage's mip chain can be expanded");
|
|
AZ_Assert(mipChainIndex < m_mipChains.size(), "Exceeded number of mip chains.");
|
|
|
|
// Expand operation - queue streaming of mip chains up to target mip chain index.
|
|
if (m_state.m_streamingTarget > mipChainIndex)
|
|
{
|
|
// Start on the next-detailed chain from the streaming target.
|
|
const size_t mipChainBegin = m_state.m_streamingTarget - 1;
|
|
const size_t mipChainEnd = mipChainIndex - 1;
|
|
|
|
// Iterate through to the end chain and queue loading operations on the mip assets.
|
|
for (size_t i = mipChainBegin; i != mipChainEnd; --i)
|
|
{
|
|
FetchMipChainAsset(i);
|
|
}
|
|
|
|
m_state.m_streamingTarget = static_cast<uint16_t>(mipChainIndex);
|
|
}
|
|
}
|
|
|
|
void StreamingImage::QueueExpandToNextMipChainLevel()
|
|
{
|
|
// Return if already reach the end
|
|
if (m_state.m_streamingTarget == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
QueueExpandToMipChainLevel(m_state.m_streamingTarget - 1);
|
|
}
|
|
|
|
RHI::ResultCode StreamingImage::ExpandMipChain()
|
|
{
|
|
AZ_Assert(m_state.m_streamingTarget <= m_state.m_residencyTarget, "The target mip chain cannot be less detailed than the resident mip chain.")
|
|
|
|
RHI::ResultCode resultCode = RHI::ResultCode::Success;
|
|
|
|
if (m_state.m_streamingTarget < m_state.m_residencyTarget)
|
|
{
|
|
#ifdef AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
AZ_TracePrintf("StreamingImage", "Expand image [%s]\n", GetImage()->GetName().data());
|
|
#endif
|
|
|
|
// Start by assuming we can expand residency to the full target range.
|
|
uint16_t mipChainIndexFound = m_state.m_streamingTarget;
|
|
|
|
// Walk the mip chains from most to least detailed, and track the latest instance
|
|
// of an unloaded mip chain. This incrementally shortens the interval.
|
|
for (uint16_t i = m_state.m_streamingTarget; i < m_state.m_residencyTarget; ++i)
|
|
{
|
|
// Can't expand to this chain, select the next one as the candidate.
|
|
if (!IsMipChainAssetReady(i))
|
|
{
|
|
mipChainIndexFound = i + 1;
|
|
}
|
|
}
|
|
|
|
// If we found a range of loaded mip chains, upload them from the low level mipchain to high level mipchain
|
|
// which the index should be from higher value to lower value
|
|
if (mipChainIndexFound != m_state.m_residencyTarget)
|
|
{
|
|
for (uint16_t mipChainIndex = m_state.m_residencyTarget-1;
|
|
mipChainIndex >= mipChainIndexFound && resultCode == RHI::ResultCode::Success;
|
|
mipChainIndex--)
|
|
{
|
|
resultCode = UploadMipChain(mipChainIndex);
|
|
if (mipChainIndex == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
m_state.m_residencyTarget = mipChainIndexFound;
|
|
}
|
|
}
|
|
|
|
return resultCode;
|
|
}
|
|
|
|
void StreamingImage::EvictMipChainAsset(size_t mipChainIndex)
|
|
{
|
|
AZ_Assert(mipChainIndex < m_mipChains.size(), "Exceeded total number of mip chains.");
|
|
|
|
const uint16_t mipChainBit = static_cast<uint16_t>(1 << mipChainIndex);
|
|
|
|
const bool isMipChainActive = RHI::CheckBitsAll(m_state.m_maskActive, mipChainBit);
|
|
const bool isMipChainEvictable = RHI::CheckBitsAll(m_state.m_maskEvictable, mipChainBit);
|
|
|
|
if (isMipChainActive && isMipChainEvictable)
|
|
{
|
|
const uint32_t mipChainMask = ~mipChainBit;
|
|
m_state.m_maskActive &= mipChainMask;
|
|
m_state.m_maskReady &= mipChainMask;
|
|
|
|
Data::Asset<ImageMipChainAsset>& mipChainAsset = m_mipChains[mipChainIndex];
|
|
AZ_Assert(mipChainAsset.GetStatus() != Data::AssetData::AssetStatus::NotLoaded, "Asset marked as active, but mipChainAsset in 'NotLoaded' state.");
|
|
Data::AssetBus::MultiHandler::BusDisconnect(mipChainAsset.GetId());
|
|
mipChainAsset.Release();
|
|
}
|
|
}
|
|
|
|
void StreamingImage::FetchMipChainAsset(size_t mipChainIndex)
|
|
{
|
|
AZ_Assert(mipChainIndex < m_mipChains.size(), "Exceeded total number of mip chains.");
|
|
|
|
const uint16_t mipChainBit = static_cast<uint16_t>(1 << mipChainIndex);
|
|
|
|
const bool isMipChainActive = RHI::CheckBitsAll(m_state.m_maskActive, mipChainBit);
|
|
|
|
if (!isMipChainActive)
|
|
{
|
|
m_state.m_maskActive |= mipChainBit;
|
|
|
|
Data::Asset<ImageMipChainAsset>& mipChainAsset = m_mipChains[mipChainIndex];
|
|
AZ_Assert(mipChainAsset.Get() == nullptr, "Asset marked as inactive, but has a valid reference.");
|
|
|
|
// Connect to the AssetBus so we are ready to receive OnAssetReady(), which will call OnMipChainAssetReady().
|
|
// If the asset happens to already be loaded, OnAssetReady() will be called immediately.
|
|
Data::AssetBus::MultiHandler::BusConnect(mipChainAsset.GetId());
|
|
|
|
// And we request that the asset be loaded in case it isn't already.
|
|
mipChainAsset.QueueLoad();
|
|
|
|
#ifdef AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
AZ_TracePrintf("StreamingImage", "Fetch mip chain asset [%s]\n", mipChainAsset.GetHint().c_str());
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "FetchMipChainAsset called for a mip chain that was already active.");
|
|
}
|
|
}
|
|
|
|
bool StreamingImage::IsMipChainAssetReady(size_t mipChainIndex) const
|
|
{
|
|
AZ_Assert(mipChainIndex < m_mipChains.size(), "Exceeded total number of mip chains.");
|
|
|
|
return RHI::CheckBitsAny(m_state.m_maskReady, static_cast<uint16_t>(1 << mipChainIndex));
|
|
}
|
|
|
|
void StreamingImage::OnMipChainAssetReady(size_t mipChainIndex)
|
|
{
|
|
AZ_Assert(mipChainIndex < m_mipChains.size(), "Exceeded total number of mip chains.");
|
|
|
|
const uint16_t mipChainBit = static_cast<uint16_t>(1 << mipChainIndex);
|
|
|
|
AZ_Assert(RHI::CheckBitsAll(m_state.m_maskActive, mipChainBit), "Mip chain should be marked as active.");
|
|
|
|
m_state.m_maskReady |= mipChainBit;
|
|
|
|
if (m_streamingController)
|
|
{
|
|
m_streamingController->OnMipChainAssetReady(this);
|
|
}
|
|
}
|
|
|
|
RHI::ResultCode StreamingImage::UploadMipChain(size_t mipChainIndex)
|
|
{
|
|
if (const Data::Asset<ImageMipChainAsset>& mipChainAsset = m_mipChains[mipChainIndex])
|
|
{
|
|
const auto& mipSlices = mipChainAsset->GetMipSlices();
|
|
|
|
RHI::StreamingImageExpandRequest request;
|
|
request.m_image = GetRHIImage();
|
|
request.m_mipSlices = mipSlices;
|
|
|
|
request.m_completeCallback = [=]()
|
|
{
|
|
#ifdef AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
AZ_TracePrintf("StreamingImage", "Upload mipchain done [%s]\n", mipChainAsset.GetHint().c_str());
|
|
#endif
|
|
EvictMipChainAsset(mipChainIndex);
|
|
};
|
|
|
|
#ifdef AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
AZ_TracePrintf("StreamingImage", "Start Upload mipchain [%d] [%s] start [%d], resident [%d]\n", mipChainIndex,
|
|
mipChainAsset.GetHint().c_str(), m_image->GetResidentMipLevel());
|
|
#endif
|
|
|
|
return m_rhiPool->ExpandImage(request);
|
|
}
|
|
return RHI::ResultCode::InvalidOperation;
|
|
}
|
|
|
|
void StreamingImage::OnAssetReady(Data::Asset<Data::AssetData> asset)
|
|
{
|
|
size_t mipChainIndex = 0;
|
|
|
|
const size_t mipChainCount = m_mipChains.size();
|
|
|
|
for (; mipChainIndex < mipChainCount; ++mipChainIndex)
|
|
{
|
|
if (m_mipChains[mipChainIndex] == asset)
|
|
{
|
|
#ifdef AZ_RPI_STREAMING_IMAGE_DEBUG_LOG
|
|
AZ_TracePrintf("StreamingImage", "mip chain asset ready [%s]\n", asset.GetHint().c_str());
|
|
#endif
|
|
OnMipChainAssetReady(mipChainIndex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void StreamingImage::OnAssetReloaded(Data::Asset<Data::AssetData> asset)
|
|
{
|
|
#if defined (AZ_RPI_STREAMING_IMAGE_HOT_RELOADING)
|
|
if (asset.GetId() == GetAssetId())
|
|
{
|
|
StreamingImageAsset* imageAsset = azrtti_cast<StreamingImageAsset*>(asset.GetData());
|
|
|
|
// Release the loaded mipchain assets from both current asset and new asset since they are coming from old asset
|
|
// This is due to we have to use PreLoad dependecy load behavior for streaming image asset
|
|
// before we can switch load behavior at runtime.
|
|
// [GFX TODO] [ATOM-14467] Remove unnecessary code in StreamingImage::OnAssetReloaded when runtime switching dependency load behavior is supported
|
|
m_imageAsset->ReleaseMipChainAssets();
|
|
imageAsset->ReleaseMipChainAssets();
|
|
|
|
// Re-initialize the image.
|
|
Shutdown();
|
|
[[maybe_unused]] RHI::ResultCode resultCode = Init(*imageAsset);
|
|
|
|
AZ_Assert(resultCode == RHI::ResultCode::Success, "Failed to re-initialize streaming image");
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "The mip chain asset auto-reload was disabled. If you are sure you want to reload mip chain manually you can remove this assert");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
const Data::Instance<StreamingImagePool>& StreamingImage::GetPool() const
|
|
{
|
|
return m_pool;
|
|
}
|
|
|
|
bool StreamingImage::IsStreamable() const
|
|
{
|
|
return (RHI::CheckBitsAny(m_imageAsset->GetFlags(), StreamingImageFlags::NotStreamable) == false);
|
|
}
|
|
|
|
}
|
|
}
|