Skip to content

Commit

Permalink
Merge pull request #2217 from billhollings/avoid-managed-mem-on-apple…
Browse files Browse the repository at this point in the history
…-silicon

On macOS Apple Silicon, avoid managed-memory textures, and resource syncs.
  • Loading branch information
billhollings authored May 1, 2024
2 parents 6c68ba1 + 607aaff commit 0d62a42
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 74 deletions.
3 changes: 0 additions & 3 deletions MoltenVK/MoltenVK/API/mvk_datatypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -482,9 +482,6 @@ static inline VkExtent3D mvkVkExtent3DFromMTLSize(MTLSize mtlSize) {
/** Macro indicating the Vulkan memory type bits corresponding to Metal memoryless memory (not host visible and lazily allocated). */
#define MVK_VK_MEMORY_TYPE_METAL_MEMORYLESS (VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)

/** Returns the Metal storage mode corresponding to the specified Vulkan memory flags. */
MTLStorageMode mvkMTLStorageModeFromVkMemoryPropertyFlags(VkMemoryPropertyFlags vkFlags);

/** Returns the Metal CPU cache mode corresponding to the specified Vulkan memory flags. */
MTLCPUCacheMode mvkMTLCPUCacheModeFromVkMemoryPropertyFlags(VkMemoryPropertyFlags vkFlags);

Expand Down
13 changes: 8 additions & 5 deletions MoltenVK/MoltenVK/GPUObjects/MVKBuffer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@

#if MVK_MACOS
if (_deviceMemory) {
_isHostCoherentTexelBuffer = !_device->_pMetalFeatures->sharedLinearTextures && _deviceMemory->isMemoryHostCoherent() && mvkIsAnyFlagEnabled(_usage, VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT);
_isHostCoherentTexelBuffer = (!isUnifiedMemoryGPU() &&
!_device->_pMetalFeatures->sharedLinearTextures &&
_deviceMemory->isMemoryHostCoherent() &&
mvkIsAnyFlagEnabled(_usage, (VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT)));
}
#endif

Expand Down Expand Up @@ -118,7 +122,8 @@
// buffer and host memory for the purpose of the host reading texture memory.
bool MVKBuffer::needsHostReadSync(MVKPipelineBarrier& barrier) {
#if MVK_MACOS
return (mvkIsAnyFlagEnabled(barrier.dstStageMask, (VK_PIPELINE_STAGE_HOST_BIT)) &&
return (!isUnifiedMemoryGPU() &&
mvkIsAnyFlagEnabled(barrier.dstStageMask, (VK_PIPELINE_STAGE_HOST_BIT)) &&
mvkIsAnyFlagEnabled(barrier.dstAccessMask, (VK_ACCESS_HOST_READ_BIT)) &&
isMemoryHostAccessible() && (!isMemoryHostCoherent() || _isHostCoherentTexelBuffer));
#else
Expand All @@ -138,9 +143,7 @@
return false;
}

#if MVK_MACOS
bool MVKBuffer::shouldFlushHostMemory() { return _isHostCoherentTexelBuffer; }
#endif
bool MVKBuffer::shouldFlushHostMemory() { return !isUnifiedMemoryGPU() && _isHostCoherentTexelBuffer; }

// Flushes the device memory at the specified memory range into the MTLBuffer.
VkResult MVKBuffer::flushToDevice(VkDeviceSize offset, VkDeviceSize size) {
Expand Down
17 changes: 14 additions & 3 deletions MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,6 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject {
*/
uint32_t getLazilyAllocatedMemoryTypes() { return _lazilyAllocatedMemoryTypes; }

/** Returns whether this is a unified memory device. */
bool getHasUnifiedMemory();

/** Returns the external memory properties supported for buffers for the handle type. */
VkExternalMemoryProperties& getExternalBufferProperties(VkExternalMemoryHandleTypeFlagBits handleType);

Expand Down Expand Up @@ -363,6 +360,9 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject {
/** Returns whether native texture atomics are supported and should be used. */
bool useNativeTextureAtomics() { return _metalFeatures.nativeTextureAtomics; }

/** Returns the MTLStorageMode that matches the Vulkan memory property flags. */
MTLStorageMode getMTLStorageModeFromVkMemoryPropertyFlags(VkMemoryPropertyFlags vkFlags);


#pragma mark Construction

Expand All @@ -388,6 +388,7 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject {

protected:
friend class MVKDevice;
friend class MVKDeviceTrackingMixin;

void propagateDebugName() override {}
MTLFeatureSet getMaximalMTLFeatureSet();
Expand Down Expand Up @@ -443,6 +444,8 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject {
uint32_t _hostCoherentMemoryTypes;
uint32_t _privateMemoryTypes;
uint32_t _lazilyAllocatedMemoryTypes;
bool _hasUnifiedMemory = true;
bool _isAppleGPU = true;
};


Expand Down Expand Up @@ -887,6 +890,8 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject {
}

protected:
friend class MVKDeviceTrackingMixin;

void propagateDebugName() override {}
MVKBuffer* addBuffer(MVKBuffer* mvkBuff);
MVKBuffer* removeBuffer(MVKBuffer* mvkBuff);
Expand Down Expand Up @@ -956,6 +961,12 @@ class MVKDeviceTrackingMixin {
/** Returns the underlying Metal device. */
id<MTLDevice> getMTLDevice() { return _device->getMTLDevice(); }

/** Returns whether the GPU is a unified memory device. */
bool isUnifiedMemoryGPU() { return getPhysicalDevice()->_hasUnifiedMemory; }

/** Returns whether the GPU is Apple Silicon. */
bool isAppleGPU() { return getPhysicalDevice()->_isAppleGPU; }

/** Returns info about the pixel format supported by the physical device. */
MVKPixelFormats* getPixelFormats() { return _device->getPixelFormats(); }

Expand Down
77 changes: 50 additions & 27 deletions MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1765,9 +1765,7 @@
// wild temporary changes, particularly during initial queries before much GPU activity has occurred.
// On Apple GPUs, CPU & GPU timestamps are the same, and timestamp period never changes.
void MVKPhysicalDevice::updateTimestampPeriod() {
if (_properties.vendorID != kAppleVendorId &&
[_mtlDevice respondsToSelector: @selector(sampleTimestamps:gpuTimestamp:)]) {

if ( !_isAppleGPU && [_mtlDevice respondsToSelector: @selector(sampleTimestamps:gpuTimestamp:)]) {
MTLTimestamp earlierCPUTs = _prevCPUTimestamp;
MTLTimestamp earlierGPUTs = _prevGPUTimestamp;
[_mtlDevice sampleTimestamps: &_prevCPUTimestamp gpuTimestamp: &_prevGPUTimestamp];
Expand Down Expand Up @@ -1804,7 +1802,7 @@
auto* budgetProps = (VkPhysicalDeviceMemoryBudgetPropertiesEXT*)next;
mvkClear(budgetProps->heapBudget, VK_MAX_MEMORY_HEAPS);
mvkClear(budgetProps->heapUsage, VK_MAX_MEMORY_HEAPS);
if (!getHasUnifiedMemory()) {
if ( !_hasUnifiedMemory ) {
budgetProps->heapBudget[1] = (VkDeviceSize)mvkGetAvailableMemorySize();
budgetProps->heapUsage[1] = (VkDeviceSize)mvkGetUsedMemorySize();
}
Expand Down Expand Up @@ -1833,11 +1831,11 @@
_supportedExtensions(this, true),
_pixelFormats(this) { // Set after _mtlDevice

initMTLDevice();
initProperties(); // Call first.
initMetalFeatures(); // Call second.
initFeatures(); // Call third.
initLimits(); // Call fourth.
initMTLDevice(); // Call first.
initProperties(); // Call second.
initMetalFeatures(); // Call third.
initFeatures(); // Call fourth.
initLimits(); // Call fifth.
initExtensions();
initMemoryProperties();
initExternalMemoryProperties();
Expand All @@ -1847,12 +1845,21 @@
}

void MVKPhysicalDevice::initMTLDevice() {
#if MVK_XCODE_14_3 && MVK_MACOS && !MVK_MACCAT
#if MVK_MACOS
_isAppleGPU = supportsMTLGPUFamily(Apple1);

// Apple Silicon will respond false to isLowPower, but never hits it.
_hasUnifiedMemory = ([_mtlDevice respondsToSelector: @selector(hasUnifiedMemory)]
? _mtlDevice.hasUnifiedMemory : _mtlDevice.isLowPower);

#if MVK_XCODE_14_3 && !MVK_MACCAT
if ([_mtlDevice respondsToSelector: @selector(setShouldMaximizeConcurrentCompilation:)]) {
[_mtlDevice setShouldMaximizeConcurrentCompilation: getMVKConfig().shouldMaximizeConcurrentCompilation];
MVKLogInfoIf(getMVKConfig().debugMode, "maximumConcurrentCompilationTaskCount %lu", _mtlDevice.maximumConcurrentCompilationTaskCount);
}
#endif

#endif // MVK_MACOS
}

// Initializes the physical device properties (except limits).
Expand Down Expand Up @@ -2968,16 +2975,14 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope
}

void MVKPhysicalDevice::initGPUInfoProperties() {

bool isIntegrated = getHasUnifiedMemory();
_properties.deviceType = isIntegrated ? VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU : VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
_properties.deviceType = _hasUnifiedMemory ? VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU : VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
strlcpy(_properties.deviceName, _mtlDevice.name.UTF8String, VK_MAX_PHYSICAL_DEVICE_NAME_SIZE);

// For Apple Silicon, the Device ID is determined by the highest
// GPU capability, which is a combination of OS version and GPU type.
// We determine Apple Silicon directly from the GPU, instead
// of from the build, in case we are running Rosetta2.
if (supportsMTLGPUFamily(Apple1)) {
if (_isAppleGPU) {
_properties.vendorID = kAppleVendorId;
_properties.deviceID = getHighestGPUCapability();
return;
Expand Down Expand Up @@ -3012,9 +3017,9 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope
if (mvkGetEntryProperty(entry, CFSTR("class-code")) == 0x30000) { // 0x30000 : DISPLAY_VGA

// The Intel GPU will always be marked as integrated.
// Return on a match of either Intel && low power, or non-Intel and non-low-power.
// Return on a match of either Intel && unified memory, or non-Intel and non-unified memory.
uint32_t vendorID = mvkGetEntryProperty(entry, CFSTR("vendor-id"));
if ( (vendorID == kIntelVendorId) == isIntegrated) {
if ( (vendorID == kIntelVendorId) == _hasUnifiedMemory) {
isFound = true;
_properties.vendorID = vendorID;
_properties.deviceID = mvkGetEntryProperty(entry, CFSTR("device-id"));
Expand Down Expand Up @@ -3168,7 +3173,7 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope
// Optional second heap for shared memory
uint32_t sharedHeapIdx;
VkMemoryPropertyFlags sharedTypePropFlags;
if (getHasUnifiedMemory()) {
if (_hasUnifiedMemory) {
// Shared memory goes in the single main heap in unified memory, and per Vulkan spec must be marked local
sharedHeapIdx = mainHeapIdx;
sharedTypePropFlags = MVK_VK_MEMORY_TYPE_METAL_SHARED | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
Expand All @@ -3194,12 +3199,14 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope
setMemoryType(typeIdx, sharedHeapIdx, sharedTypePropFlags);
typeIdx++;

// Managed storage
// Managed storage. On all Apple Silicon, use Shared instead.
uint32_t managedBit = 0;
#if MVK_MACOS
managedBit = 1 << typeIdx;
setMemoryType(typeIdx, mainHeapIdx, MVK_VK_MEMORY_TYPE_METAL_MANAGED);
typeIdx++;
if ( !_isAppleGPU ) {
managedBit = 1 << typeIdx;
setMemoryType(typeIdx, mainHeapIdx, MVK_VK_MEMORY_TYPE_METAL_MANAGED);
typeIdx++;
}
#endif

// Memoryless storage
Expand Down Expand Up @@ -3235,17 +3242,33 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope
_allMemoryTypes = privateBit | sharedBit | managedBit | memlessBit;
}

bool MVKPhysicalDevice::getHasUnifiedMemory() {
MVK_PUBLIC_SYMBOL MTLStorageMode MVKPhysicalDevice::getMTLStorageModeFromVkMemoryPropertyFlags(VkMemoryPropertyFlags vkFlags) {

// If not visible to the host, use Private, or Memoryless if available and lazily allocated.
if ( !mvkAreAllFlagsEnabled(vkFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) ) {
#if MVK_APPLE_SILICON
if (mvkAreAllFlagsEnabled(vkFlags, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)) {
return MTLStorageModeMemoryless;
}
#endif
return MTLStorageModePrivate;
}

// If visible to the host and coherent: Shared
if (mvkAreAllFlagsEnabled(vkFlags, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
return MTLStorageModeShared;
}

// If visible to the host, but not coherent: Shared on Apple Silicon, Managed on other GPUs.
#if MVK_MACOS
return ([_mtlDevice respondsToSelector: @selector(hasUnifiedMemory)]
? _mtlDevice.hasUnifiedMemory : _mtlDevice.isLowPower);
return _isAppleGPU ? MTLStorageModeShared : MTLStorageModeManaged;
#else
return true;
return MTLStorageModeShared;
#endif
}

uint64_t MVKPhysicalDevice::getVRAMSize() {
if (getHasUnifiedMemory()) {
if (_hasUnifiedMemory) {
return mvkGetSystemMemorySize();
} else {
// There's actually no way to query the total physical VRAM on the device in Metal.
Expand Down Expand Up @@ -3408,7 +3431,7 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope
switch (getMVKConfig().semaphoreSupportStyle) {
case MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_METAL_EVENTS_WHERE_SAFE: {
bool isNVIDIA = _properties.vendorID == kNVVendorId;
bool isRosetta2 = _properties.vendorID == kAppleVendorId && !MVK_APPLE_SILICON;
bool isRosetta2 = _isAppleGPU && !MVK_APPLE_SILICON;
if (_metalFeatures.events && !(isRosetta2 || isNVIDIA)) { _vkSemaphoreStyle = MVKSemaphoreStyleUseMTLEvent; }
break;
}
Expand Down
6 changes: 3 additions & 3 deletions MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
if (memSize == 0 || !isMemoryHostAccessible()) { return VK_SUCCESS; }

#if MVK_MACOS
if (_mtlBuffer && _mtlStorageMode == MTLStorageModeManaged) {
if ( !isUnifiedMemoryGPU() && _mtlBuffer && _mtlStorageMode == MTLStorageModeManaged) {
[_mtlBuffer didModifyRange: NSMakeRange(offset, memSize)];
}
#endif
Expand All @@ -106,7 +106,7 @@
if (memSize == 0 || !isMemoryHostAccessible()) { return VK_SUCCESS; }

#if MVK_MACOS
if (pBlitEnc && _mtlBuffer && _mtlStorageMode == MTLStorageModeManaged) {
if ( !isUnifiedMemoryGPU() && pBlitEnc && _mtlBuffer && _mtlStorageMode == MTLStorageModeManaged) {
if ( !pBlitEnc->mtlCmdBuffer) { pBlitEnc->mtlCmdBuffer = _device->getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseInvalidateMappedMemoryRanges); }
if ( !pBlitEnc->mtlBlitEncoder) { pBlitEnc->mtlBlitEncoder = [pBlitEnc->mtlCmdBuffer blitCommandEncoder]; }
[pBlitEnc->mtlBlitEncoder synchronizeResource: _mtlBuffer];
Expand Down Expand Up @@ -285,7 +285,7 @@
// Set Metal memory parameters
_vkMemAllocFlags = 0;
_vkMemPropFlags = _device->_pMemoryProperties->memoryTypes[pAllocateInfo->memoryTypeIndex].propertyFlags;
_mtlStorageMode = mvkMTLStorageModeFromVkMemoryPropertyFlags(_vkMemPropFlags);
_mtlStorageMode = getPhysicalDevice()->getMTLStorageModeFromVkMemoryPropertyFlags(_vkMemPropFlags);
_mtlCPUCacheMode = mvkMTLCPUCacheModeFromVkMemoryPropertyFlags(_vkMemPropFlags);

_allocationSize = pAllocateInfo->allocationSize;
Expand Down
17 changes: 9 additions & 8 deletions MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
Original file line number Diff line number Diff line change
Expand Up @@ -469,18 +469,18 @@
// texture and host memory for the purpose of the host reading texture memory.
bool MVKImageMemoryBinding::needsHostReadSync(MVKPipelineBarrier& barrier) {
#if MVK_MACOS
return ((barrier.newLayout == VK_IMAGE_LAYOUT_GENERAL) &&
return ( !isUnifiedMemoryGPU() && (barrier.newLayout == VK_IMAGE_LAYOUT_GENERAL) &&
mvkIsAnyFlagEnabled(barrier.dstAccessMask, (VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT)) &&
isMemoryHostAccessible() && (!_device->_pMetalFeatures->sharedLinearTextures || !isMemoryHostCoherent()));
#else
return false;
return false;
#endif
}

bool MVKImageMemoryBinding::shouldFlushHostMemory() { return isMemoryHostAccessible() && (!_mtlTexelBuffer || _ownsTexelBuffer); }

// Flushes the device memory at the specified memory range into the MTLTexture. Updates
// all subresources that overlap the specified range and are in an updatable layout state.
// Flushes the memory at the specified memory range into the MTLTexture.
// Updates all subresources that overlap the specified range and are in an updatable layout state.
VkResult MVKImageMemoryBinding::flushToDevice(VkDeviceSize offset, VkDeviceSize size) {
if (shouldFlushHostMemory()) {
for(uint8_t planeIndex = beginPlaneIndex(); planeIndex < endPlaneIndex(); planeIndex++) {
Expand All @@ -501,7 +501,7 @@
return VK_SUCCESS;
}

// Pulls content from the MTLTexture into the device memory at the specified memory range.
// Pulls content from the MTLTexture into memory at the specified memory range.
// Pulls from all subresources that overlap the specified range and are in an updatable layout state.
VkResult MVKImageMemoryBinding::pullFromDevice(VkDeviceSize offset, VkDeviceSize size) {
if (shouldFlushHostMemory()) {
Expand Down Expand Up @@ -715,7 +715,7 @@ static MTLRegion getMTLRegion(const ImgRgn& imgRgn) {
#if MVK_MACOS
// On macOS, if the device doesn't have unified memory, and the texture is using managed memory, we need
// to sync the managed memory from the GPU, so the texture content is accessible to be copied by the CPU.
if ( !getPhysicalDevice()->getHasUnifiedMemory() && getMTLStorageMode() == MTLStorageModeManaged ) {
if ( !isUnifiedMemoryGPU() && getMTLStorageMode() == MTLStorageModeManaged ) {
@autoreleasepool {
id<MTLCommandBuffer> mtlCmdBuff = getDevice()->getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseCopyImageToMemory);
id<MTLBlitCommandEncoder> mtlBlitEnc = [mtlCmdBuff blitCommandEncoder];
Expand Down Expand Up @@ -858,9 +858,9 @@ static MTLRegion getMTLRegion(const ImgRgn& imgRgn) {
pMemoryRequirements->memoryTypeBits = (_isDepthStencilAttachment)
? mvkPD->getPrivateMemoryTypes()
: mvkPD->getAllMemoryTypes();
// Metal on non-Apple GPUs does not provide native support for host-coherent memory, but Vulkan requires it for Linear images
#if MVK_MACOS
// Metal on macOS does not provide native support for host-coherent memory, but Vulkan requires it for Linear images
if ( !_isLinear ) {
if ( !isAppleGPU() && !_isLinear ) {
mvkDisableFlags(pMemoryRequirements->memoryTypeBits, mvkPD->getHostCoherentMemoryTypes());
}
#endif
Expand Down Expand Up @@ -1052,6 +1052,7 @@ static MTLRegion getMTLRegion(const ImgRgn& imgRgn) {

#if MVK_MACOS
// For macOS prior to 10.15.5, textures cannot use Shared storage mode, so change to Managed storage mode.
// All Apple GPUs support shared linear textures, so this only applies to other GPUs.
if (stgMode == MTLStorageModeShared && !_device->_pMetalFeatures->sharedLinearTextures) {
stgMode = MTLStorageModeManaged;
}
Expand Down
25 changes: 0 additions & 25 deletions MoltenVK/MoltenVK/Vulkan/mvk_datatypes.mm
Original file line number Diff line number Diff line change
Expand Up @@ -882,31 +882,6 @@ MVK_PUBLIC_SYMBOL CGRect mvkCGRectFromVkRectLayerKHR(VkRectLayerKHR vkRect) {
#pragma mark -
#pragma mark Memory options

MVK_PUBLIC_SYMBOL MTLStorageMode mvkMTLStorageModeFromVkMemoryPropertyFlags(VkMemoryPropertyFlags vkFlags) {

// If not visible to the host, use Private, or Memoryless if available and lazily allocated.
if ( !mvkAreAllFlagsEnabled(vkFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) ) {
#if MVK_APPLE_SILICON
if (mvkAreAllFlagsEnabled(vkFlags, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)) {
return MTLStorageModeMemoryless;
}
#endif
return MTLStorageModePrivate;
}

// If visible to the host and coherent: Shared
if (mvkAreAllFlagsEnabled(vkFlags, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
return MTLStorageModeShared;
}

// If visible to the host, and not coherent: Managed on macOS, Shared on iOS
#if MVK_MACOS
return MTLStorageModeManaged;
#else
return MTLStorageModeShared;
#endif
}

MVK_PUBLIC_SYMBOL MTLCPUCacheMode mvkMTLCPUCacheModeFromVkMemoryPropertyFlags(VkMemoryPropertyFlags vkFlags) {
return MTLCPUCacheModeDefaultCache;
}
Expand Down

0 comments on commit 0d62a42

Please sign in to comment.