Fix issue where mapped host-coherent device memory not updated from image contents on macOS.

Currently, when a mapping is initiated for host-coherent device memory, existing
image contents are copied from the MTLTexture to host memory. However, if
host-coherent device memory is already mapped in a long-standing memory mapping,
changes to MTLTexture content are not reflected to the host memory.

This change adds that capability to open memory mappings. When the image pipeline
barrier is applied to an image that is attached to a host-coherent device memory
that currently has an open memory mapping, the MTLTexture contents are copied to
the open host memory binding.

Add MVKMappedMemoryRange to retrieve the currently mapped range in an MVKDeviceMemory.

MVKImage cleanup use of isHostCoherent(). Let it be same as MVKDeviceMemory
and don't test for it from macOS because it is unnecessary.
This commit is contained in:
Bill Hollings 2020-06-15 22:18:53 -04:00
parent c1c49def22
commit 205b859580
5 changed files with 78 additions and 39 deletions

View File

@ -20,8 +20,9 @@ Released TBD
- Add support for extensions:
- VK_KHR_sampler_ycbcr_conversion
- Remove use of `@available()` directive as it was causing issues in some build environments
- Refactor **MoltenVK** *Xcode* build architectures
- Fix issue where mapped host-coherent device memory not updated from image contents on *macOS*.
- Remove use of `@available()` directive as it was causing issues in some build environments.
- Refactor **MoltenVK** *Xcode* build architectures.
- Demo `API-Samples generateSPIRVShaders` no longer builds `MoltenVKShaderController` tool.

View File

@ -33,6 +33,12 @@ static const VkExternalMemoryHandleTypeFlagBits VK_EXTERNAL_MEMORY_HANDLE_TYPE_M
#pragma mark MVKDeviceMemory
typedef struct {
VkDeviceSize offset = 0;
VkDeviceSize size = 0;
} MVKMappedMemoryRange;
/** Represents a Vulkan device-space memory allocation. */
class MVKDeviceMemory : public MVKVulkanAPIDeviceObject {
@ -77,6 +83,16 @@ public:
/** Unmaps a previously mapped memory range. */
void unmap();
/**
* If this device memory is currently mapped to host memory, returns the range within
* this device memory that is currently mapped to host memory, or returns {0,0} if
* this device memory is not currently mapped to host memory.
*/
inline const MVKMappedMemoryRange& getMappedRange() { return _mappedRange; }
/** Returns whether this device memory is currently mapped to host memory. */
bool isMapped() { return _mappedRange.size > 0; }
/**
* If this memory is host-visible, the specified memory range is flushed to the device.
* Normally, flushing will only occur if the device memory is non-coherent, but flushing
@ -150,13 +166,11 @@ protected:
MVKSmallVector<MVKImageMemoryBinding*, 4> _imageMemoryBindings;
std::mutex _rezLock;
VkDeviceSize _allocationSize = 0;
VkDeviceSize _mapOffset = 0;
VkDeviceSize _mapSize = 0;
MVKMappedMemoryRange _mappedRange;
id<MTLBuffer> _mtlBuffer = nil;
id<MTLHeap> _mtlHeap = nil;
void* _pMemory = nullptr;
void* _pHostMemory = nullptr;
bool _isMapped = false;
bool _isDedicated = false;
MTLStorageMode _mtlStorageMode;
MTLCPUCacheMode _mtlCPUCacheMode;

View File

@ -43,7 +43,7 @@ VkResult MVKDeviceMemory::map(VkDeviceSize offset, VkDeviceSize size, VkMemoryMa
return reportError(VK_ERROR_MEMORY_MAP_FAILED, "Private GPU-only memory cannot be mapped to host memory.");
}
if (_isMapped) {
if (isMapped()) {
return reportError(VK_ERROR_MEMORY_MAP_FAILED, "Memory is already mapped. Call vkUnmapMemory() first.");
}
@ -51,9 +51,8 @@ VkResult MVKDeviceMemory::map(VkDeviceSize offset, VkDeviceSize size, VkMemoryMa
return reportError(VK_ERROR_OUT_OF_HOST_MEMORY, "Could not allocate %llu bytes of host-accessible device memory.", _allocationSize);
}
_mapOffset = offset;
_mapSize = adjustMemorySize(size, offset);
_isMapped = true;
_mappedRange.offset = offset;
_mappedRange.size = adjustMemorySize(size, offset);
*ppData = (void*)((uintptr_t)_pMemory + offset);
@ -65,17 +64,16 @@ VkResult MVKDeviceMemory::map(VkDeviceSize offset, VkDeviceSize size, VkMemoryMa
void MVKDeviceMemory::unmap() {
if ( !_isMapped ) {
if ( !isMapped() ) {
reportError(VK_ERROR_MEMORY_MAP_FAILED, "Memory is not mapped. Call vkMapMemory() first.");
return;
}
// Coherent memory does not require flushing by app, so we must flush now.
flushToDevice(_mapOffset, _mapSize, isMemoryHostCoherent());
flushToDevice(_mappedRange.offset, _mappedRange.size, isMemoryHostCoherent());
_mapOffset = 0;
_mapSize = 0;
_isMapped = false;
_mappedRange.offset = 0;
_mappedRange.size = 0;
}
VkResult MVKDeviceMemory::flushToDevice(VkDeviceSize offset, VkDeviceSize size, bool evenIfCoherent) {

View File

@ -70,6 +70,7 @@ protected:
MVKImageSubresource* getSubresource(uint32_t mipLevel, uint32_t arrayLayer);
void updateMTLTextureContent(MVKImageSubresource& subresource, VkDeviceSize offset, VkDeviceSize size);
void getMTLTextureContent(MVKImageSubresource& subresource, VkDeviceSize offset, VkDeviceSize size);
bool overlaps(VkSubresourceLayout& imgLayout, VkDeviceSize offset, VkDeviceSize size);
void propagateDebugName();
MVKImageMemoryBinding* getMemoryBinding() const;
void applyImageMemoryBarrier(VkPipelineStageFlags srcStageMask,
@ -77,6 +78,9 @@ protected:
MVKPipelineBarrier& barrier,
MVKCommandEncoder* cmdEncoder,
MVKCommandUse cmdUse);
void pullFromDeviceOnCompletion(MVKCommandEncoder* cmdEncoder,
MVKImageSubresource& subresource,
const MVKMappedMemoryRange& mappedRange);
MVKImagePlane(MVKImage* image, uint8_t planeIndex);
@ -119,7 +123,7 @@ public:
MVKPipelineBarrier& barrier,
MVKCommandEncoder* cmdEncoder,
MVKCommandUse cmdUse) override;
~MVKImageMemoryBinding();
protected:
@ -315,12 +319,6 @@ public:
/** Returns the Metal CPU cache mode used by this image. */
MTLCPUCacheMode getMTLCPUCacheMode();
/**
* Returns whether the memory is automatically coherent between device and host.
* On macOS, this always returns false because textures cannot use Shared storage mode.
*/
bool isMemoryHostCoherent();
#pragma mark Construction
MVKImage(MVKDevice* device, const VkImageCreateInfo* pCreateInfo);

View File

@ -177,11 +177,7 @@ void MVKImagePlane::updateMTLTextureContent(MVKImageSubresource& subresource,
VkSubresourceLayout& imgLayout = subresource.layout;
// Check if subresource overlaps the memory range.
VkDeviceSize memStart = offset;
VkDeviceSize memEnd = offset + size;
VkDeviceSize imgStart = imgLayout.offset;
VkDeviceSize imgEnd = imgLayout.offset + imgLayout.size;
if (imgStart >= memEnd || imgEnd <= memStart) { return; }
if ( !overlaps(imgLayout, offset, size) ) { return; }
// Don't update if host memory has not been mapped yet.
void* pHostMem = getMemoryBinding()->getHostMemoryAddress();
@ -242,11 +238,7 @@ void MVKImagePlane::getMTLTextureContent(MVKImageSubresource& subresource,
VkSubresourceLayout& imgLayout = subresource.layout;
// Check if subresource overlaps the memory range.
VkDeviceSize memStart = offset;
VkDeviceSize memEnd = offset + size;
VkDeviceSize imgStart = imgLayout.offset;
VkDeviceSize imgEnd = imgLayout.offset + imgLayout.size;
if (imgStart >= memEnd || imgEnd <= memStart) { return; }
if ( !overlaps(imgLayout, offset, size) ) { return; }
// Don't update if host memory has not been mapped yet.
void* pHostMem = getMemoryBinding()->getHostMemoryAddress();
@ -271,6 +263,15 @@ void MVKImagePlane::getMTLTextureContent(MVKImageSubresource& subresource,
slice: imgSubRez.arrayLayer];
}
// Returns whether subresource layout overlaps the memory range.
bool MVKImagePlane::overlaps(VkSubresourceLayout& imgLayout, VkDeviceSize offset, VkDeviceSize size) {
VkDeviceSize memStart = offset;
VkDeviceSize memEnd = offset + size;
VkDeviceSize imgStart = imgLayout.offset;
VkDeviceSize imgEnd = imgLayout.offset + imgLayout.size;
return imgStart < memEnd && imgEnd > memStart;
}
void MVKImagePlane::propagateDebugName() {
setLabelIfNotNil(_image->_planes[_planeIndex]->_mtlTexture, _image->_debugName);
}
@ -297,20 +298,50 @@ void MVKImagePlane::applyImageMemoryBarrier(VkPipelineStageFlags srcStageMask,
? _image->getLayerCount()
: (layerStart + barrier.layerCount));
MVKImageMemoryBinding* memBind = getMemoryBinding();
bool needsSync = memBind->needsHostReadSync(srcStageMask, dstStageMask, barrier);
bool needsPull = (!memBind->_usesTexelBuffer &&
memBind->isMemoryHostCoherent() &&
barrier.newLayout == VK_IMAGE_LAYOUT_GENERAL &&
mvkIsAnyFlagEnabled(barrier.dstAccessMask, (VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT)));
MVKDeviceMemory* dvcMem = memBind->getDeviceMemory();
const MVKMappedMemoryRange& mappedRange = dvcMem ? dvcMem->getMappedRange() : MVKMappedMemoryRange();
// Iterate across mipmap levels and layers, and update the image layout state for each
for (uint32_t mipLvl = mipLvlStart; mipLvl < mipLvlEnd; mipLvl++) {
for (uint32_t layer = layerStart; layer < layerEnd; layer++) {
MVKImageSubresource* pImgRez = getSubresource(mipLvl, layer);
if (pImgRez) { pImgRez->layoutState = barrier.newLayout; }
if (needsSync) {
#if MVK_MACOS
if (getMemoryBinding()->needsHostReadSync(srcStageMask, dstStageMask, barrier)) {
[cmdEncoder->getMTLBlitEncoder(cmdUse) synchronizeTexture: _mtlTexture slice: layer level: mipLvl];
}
#endif
}
// Check if image content should be pulled into host-mapped device memory after
// the GPU is done with it. This only applies if the image is intended to be
// host-coherent, is not using a texel buffer, is transitioning to host-readable,
// AND the device memory has an already-open memory mapping. If a memory mapping is
// created later, it will pull the image contents in at that time, so it is not needed now.
// The mapped range will be {0,0} if device memory is not currently mapped.
if (needsPull && pImgRez && overlaps(pImgRez->layout, mappedRange.offset, mappedRange.size)) {
pullFromDeviceOnCompletion(cmdEncoder, *pImgRez, mappedRange);
}
}
}
}
// Once the command buffer completes, pull the content of the subresource into host memory.
// This is only necessary when the image memory is intended to be host-coherent, and the
// device memory is currently mapped to host memory
void MVKImagePlane::pullFromDeviceOnCompletion(MVKCommandEncoder* cmdEncoder,
MVKImageSubresource& subresource,
const MVKMappedMemoryRange& mappedRange) {
[cmdEncoder->_mtlCmdBuffer addCompletedHandler: ^(id<MTLCommandBuffer> mcb) {
getMTLTextureContent(subresource, mappedRange.offset, mappedRange.size);
}];
}
MVKImagePlane::MVKImagePlane(MVKImage* image, uint8_t planeIndex) {
_image = image;
_planeIndex = planeIndex;
@ -360,8 +391,7 @@ VkResult MVKImageMemoryBinding::bindDeviceMemory(MVKDeviceMemory* mvkMem, VkDevi
#if MVK_MACOS
// macOS cannot use shared memory for texel buffers.
// Test _deviceMemory->isMemoryHostCoherent() directly because local version overrides.
_usesTexelBuffer = _usesTexelBuffer && _deviceMemory && !_deviceMemory->isMemoryHostCoherent();
_usesTexelBuffer = _usesTexelBuffer && !isMemoryHostCoherent();
#endif
flushToDevice(getDeviceMemoryOffset(), getByteCount());
@ -394,9 +424,10 @@ bool MVKImageMemoryBinding::needsHostReadSync(VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
MVKPipelineBarrier& barrier) {
#if MVK_MACOS
// On macOS, texture memory is never host-coherent, so don't test for it.
return ((barrier.newLayout == VK_IMAGE_LAYOUT_GENERAL) &&
mvkIsAnyFlagEnabled(barrier.dstAccessMask, (VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT)) &&
isMemoryHostAccessible() && !isMemoryHostCoherent());
isMemoryHostAccessible());
#endif
#if MVK_IOS
return false;
@ -728,9 +759,6 @@ MTLCPUCacheMode MVKImage::getMTLCPUCacheMode() {
return _memoryBindings[0]->_deviceMemory ? _memoryBindings[0]->_deviceMemory->getMTLCPUCacheMode() : MTLCPUCacheModeDefaultCache;
}
bool MVKImage::isMemoryHostCoherent() {
return (getMTLStorageMode() == MTLStorageModeShared);
}
#pragma mark Construction