mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-07-08 08:40:55 +00:00
b4e4fed40f
* We used to allow applications to call vkFlushMappedMemoryRanges on coherent memory to manually annotate regions of memory that are changed in persistent maps, thus avoiding the overhead of RenderDoc needing to check for changes on each submit. * Unfortunately this means that if the application calls flush wrongly then changes will no longer appear, even though the application was completely correct, if misleading, since by the spec behaviour vkFlushMappedMemoryRanges is a no-op on coherent memory so incorrect calls to it make no difference. * Since applications making use of this are rare or non-existant we just remove the optimisation.
2945 lines
109 KiB
C++
2945 lines
109 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2021 Baldur Karlsson
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
******************************************************************************/
|
|
|
|
#include <algorithm>
|
|
#include "../vk_core.h"
|
|
#include "../vk_debug.h"
|
|
#include "core/settings.h"
|
|
|
|
RDOC_CONFIG(bool, Vulkan_GPUReadbackDeviceLocal, true,
|
|
"When reading back mapped device-local memory from discrete GPUs, use a GPU copy "
|
|
"instead of a CPU side comparison directly to mapped memory.");
|
|
|
|
/************************************************************************
|
|
*
|
|
* Mapping is simpler in Vulkan, at least in concept, but that comes with
|
|
* some restrictions/assumptions about behaviour or performance
|
|
* guarantees.
|
|
*
|
|
* In general we make a distinction between coherent and non-coherent
|
|
* memory, and then also consider persistent maps vs non-persistent maps.
|
|
* (Important note - there is no API concept of persistent maps, any map
|
|
* can be persistent, and we must handle this).
|
|
*
|
|
* For persistent coherent maps we have two options:
|
|
* - pass an intercepted buffer back to the application, whenever any
|
|
* changes could be GPU-visible (at least every QueueSubmit), diff the
|
|
* buffer and memcpy to the real pointer & serialise it if capturing.
|
|
* - pass the real mapped pointer back to the application. Ignore it
|
|
* until capturing, then do readback on the mapped pointer and
|
|
* diff, serialise any changes.
|
|
*
|
|
* For persistent non-coherent maps again we have two options:
|
|
* - pass an intercepted buffer back to the application. At any Flush()
|
|
* call copy the flushed region over to the real buffer and if
|
|
* capturing then serialise it.
|
|
* - pass the real mapped pointer back to the application. Ignore it
|
|
* until capturing, then serialise out any regions that are Flush()'d
|
|
* by reading back from the mapped pointer.
|
|
*
|
|
* Now consider transient (non-persistent) maps.
|
|
*
|
|
* For transient coherent maps:
|
|
* - pass an intercepted buffer back to the application, ensuring it has
|
|
* the correct current contents. Once unmapped, copy the contents to
|
|
* the real pointer and save if capturing.
|
|
* - return the real mapped pointer, and readback & save the contents on
|
|
* unmap if capturing
|
|
*
|
|
* For transient non-coherent maps:
|
|
* - pass back an intercepted buffer, again ensuring it has the correct
|
|
* current contents, and for each Flush() copy the contents to the
|
|
* real pointer and save if capturing.
|
|
* - return the real mapped pointer, and readback & save the contents on
|
|
* each flush if capturing.
|
|
*
|
|
* Note several things:
|
|
*
|
|
* The choices in each case are: Intercept & manage, vs. Lazily readback.
|
|
*
|
|
* We do not have a completely free choice. I.e. we can choose our
|
|
* behaviour based on coherency, but not on persistent vs. transient as
|
|
* we have no way to know whether any map we see will be persistent or
|
|
* not.
|
|
*
|
|
* In the transient case we must ensure the correct contents are in an
|
|
* intercepted buffer before returning to the application. Either to
|
|
* ensure the copy to real doesn't upload garbage data, or to ensure a
|
|
* diff to determine modified range is accurate. This is technically
|
|
* required for persistent maps also, but informally we think of a
|
|
* persistent map as from the beginning of the memory's lifetime so
|
|
* there are no previous contents (as above though, we cannot truly
|
|
* differentiate between transient and persistent maps).
|
|
*
|
|
* The essential tradeoff: overhead of managing intercepted buffer
|
|
* against potential cost of reading back from mapped pointer. The cost
|
|
* of reading back from the mapped pointer is essentially unknown. In
|
|
* all likelihood it will not be as cheap as reading back from a locally
|
|
* allocated intercepted buffer, but it might not be that bad. If the
|
|
* cost is low enough for mapped pointer readbacks then it's definitely
|
|
* better to do that, as it's very simple to implement and maintain
|
|
* (no complex bookkeeping of buffers) and we only pay this cost during
|
|
* frame capture, which has a looser performance requirement anyway.
|
|
*
|
|
* Note that the primary difficulty with intercepted buffers is ensuring
|
|
* they stay in sync and have the correct contents at all times. This
|
|
* must be done without readbacks otherwise there is no benefit. Even a
|
|
* DMA to a readback friendly memory type means a GPU sync which is even
|
|
* worse than reading from a mapped pointer. There is also overhead in
|
|
* keeping a copy of the buffer and constantly copying back and forth
|
|
* (potentially diff'ing the contents each time).
|
|
*
|
|
* A hybrid solution would be to use intercepted buffers for non-
|
|
* coherent memory, with the proviso that if a buffer is regularly mapped
|
|
* then we fallback to returning a direct pointer until the frame capture
|
|
* begins - if a map happens within a frame capture intercept it,
|
|
* otherwise if it was mapped before the frame resort to reading back
|
|
* from the mapped pointer. For coherent memory, always readback from the
|
|
* mapped pointer. This is similar to behaviour on D3D or GL except that
|
|
* a capture would fail if the map wasn't intercepted, rather than being
|
|
* able to fall back.
|
|
*
|
|
* This is likely the best option if avoiding readbacks is desired as the
|
|
* cost of constantly monitoring coherent maps for modifications and
|
|
* copying around is generally extremely undesirable and may well be more
|
|
* expensive than any readback cost.
|
|
*
|
|
* !!!!!!!!!!!!!!!
|
|
* The current solution is to never intercept any maps, and rely on the
|
|
* readback from memory not being too expensive and only happening during
|
|
* frame capture where such an impact is less severe (as opposed to
|
|
* reading back from this memory every frame even while idle).
|
|
* !!!!!!!!!!!!!!!
|
|
*
|
|
* If in future this changes, the above hybrid solution is the next best
|
|
* option to try to avoid most of the readbacks by using intercepted
|
|
* buffers where possible, with a fallback to mapped pointer readback if
|
|
* necessary.
|
|
*
|
|
* Note: No matter what we want to discouarge coherent persistent maps
|
|
* (coherent transient maps are less of an issue) as these must still be
|
|
* diff'd regularly during capture which has a high overhead (higher
|
|
* still if there is extra cost on the readback).
|
|
*
|
|
************************************************************************/
|
|
|
|
// Memory functions
|
|
|
|
template <>
|
|
VkBindBufferMemoryInfo *WrappedVulkan::UnwrapInfos(const VkBindBufferMemoryInfo *info, uint32_t count)
|
|
{
|
|
VkBindBufferMemoryInfo *ret = GetTempArray<VkBindBufferMemoryInfo>(count);
|
|
|
|
memcpy(ret, info, count * sizeof(VkBindBufferMemoryInfo));
|
|
|
|
for(uint32_t i = 0; i < count; i++)
|
|
{
|
|
ret[i].buffer = Unwrap(ret[i].buffer);
|
|
ret[i].memory = Unwrap(ret[i].memory);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <>
|
|
VkBindImageMemoryInfo *WrappedVulkan::UnwrapInfos(const VkBindImageMemoryInfo *info, uint32_t count)
|
|
{
|
|
size_t memSize = sizeof(VkBindImageMemoryInfo) * count;
|
|
|
|
for(uint32_t i = 0; i < count; i++)
|
|
memSize += GetNextPatchSize(info[i].pNext);
|
|
|
|
byte *tempMem = GetTempMemory(memSize);
|
|
|
|
VkBindImageMemoryInfo *ret = (VkBindImageMemoryInfo *)tempMem;
|
|
|
|
tempMem += sizeof(VkBindImageMemoryInfo) * count;
|
|
|
|
memcpy(ret, info, count * sizeof(VkBindImageMemoryInfo));
|
|
|
|
for(uint32_t i = 0; i < count; i++)
|
|
{
|
|
UnwrapNextChain(m_State, "VkBindImageMemoryInfo", tempMem, (VkBaseInStructure *)&ret[i]);
|
|
ret[i].image = Unwrap(ret[i].image);
|
|
ret[i].memory = Unwrap(ret[i].memory);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool WrappedVulkan::CheckMemoryRequirements(const char *resourceName, ResourceId memId,
|
|
VkDeviceSize memoryOffset, VkMemoryRequirements mrq,
|
|
bool external)
|
|
{
|
|
// verify that the memory meets basic requirements. If not, something changed and we should
|
|
// bail loading this capture. This is a bit of an under-estimate since we just make sure
|
|
// there's enough space left in the memory, that doesn't mean that there aren't overlaps due
|
|
// to increased size requirements.
|
|
ResourceId memOrigId = GetResourceManager()->GetOriginalID(memId);
|
|
|
|
VulkanCreationInfo::Memory &memInfo = m_CreationInfo.m_Memory[memId];
|
|
uint32_t bit = 1U << memInfo.memoryTypeIndex;
|
|
|
|
// verify type
|
|
if((mrq.memoryTypeBits & bit) == 0)
|
|
{
|
|
rdcstr bitsString;
|
|
|
|
for(uint32_t i = 0; i < 32; i++)
|
|
{
|
|
if(mrq.memoryTypeBits & (1U << i))
|
|
bitsString += StringFormat::Fmt("%s%u", bitsString.empty() ? "" : ", ", i);
|
|
}
|
|
|
|
if(external)
|
|
{
|
|
RDCERR(
|
|
"Trying to bind %s to memory %s which is type %u, "
|
|
"but only these types are allowed: %s\n"
|
|
"This resource was created with external memory bindings, which is not represented in "
|
|
"the capture.\n"
|
|
"Some drivers do not allow externally-imported resources to be bound to non-external "
|
|
"memory, meaning this cannot be replayed.",
|
|
resourceName, ToStr(memOrigId).c_str(), memInfo.memoryTypeIndex, bitsString.c_str());
|
|
m_FailedReplayStatus = ReplayStatus::APIHardwareUnsupported;
|
|
return false;
|
|
}
|
|
|
|
RDCERR(
|
|
"Trying to bind %s to memory %s which is type %u, "
|
|
"but only these types are allowed: %s\n"
|
|
"This is most likely caused by incompatible hardware or drivers between capture and "
|
|
"replay, causing a change in memory requirements.",
|
|
resourceName, ToStr(memOrigId).c_str(), memInfo.memoryTypeIndex, bitsString.c_str());
|
|
m_FailedReplayStatus = ReplayStatus::APIHardwareUnsupported;
|
|
return false;
|
|
}
|
|
|
|
// verify offset alignment
|
|
if((memoryOffset % mrq.alignment) != 0)
|
|
{
|
|
RDCERR(
|
|
"Trying to bind %s to memory %s which is type %u, "
|
|
"but offset 0x%llx doesn't satisfy alignment 0x%llx.\n"
|
|
"This is most likely caused by incompatible hardware or drivers between capture and "
|
|
"replay, causing a change in memory requirements.",
|
|
resourceName, ToStr(memOrigId).c_str(), memInfo.memoryTypeIndex, memoryOffset, mrq.alignment);
|
|
m_FailedReplayStatus = ReplayStatus::APIHardwareUnsupported;
|
|
return false;
|
|
}
|
|
|
|
// verify size
|
|
if(mrq.size > memInfo.allocSize - memoryOffset)
|
|
{
|
|
RDCERR(
|
|
"Trying to bind %s to memory %s which is type %u, "
|
|
"but at offset 0x%llx the reported size of 0x%llx won't fit the 0x%llx bytes of memory.\n"
|
|
"This is most likely caused by incompatible hardware or drivers between capture and "
|
|
"replay, causing a change in memory requirements.",
|
|
resourceName, ToStr(memOrigId).c_str(), memInfo.memoryTypeIndex, memoryOffset, mrq.size,
|
|
memInfo.allocSize);
|
|
m_FailedReplayStatus = ReplayStatus::APIHardwareUnsupported;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkAllocateMemory(SerialiserType &ser, VkDevice device,
|
|
const VkMemoryAllocateInfo *pAllocateInfo,
|
|
const VkAllocationCallbacks *pAllocator,
|
|
VkDeviceMemory *pMemory)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT_LOCAL(AllocateInfo, *pAllocateInfo).Important();
|
|
SERIALISE_ELEMENT_OPT(pAllocator);
|
|
SERIALISE_ELEMENT_LOCAL(Memory, GetResID(*pMemory)).TypedAs("VkDeviceMemory"_lit);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
VkDeviceMemory mem = VK_NULL_HANDLE;
|
|
|
|
VkMemoryAllocateInfo patched = AllocateInfo;
|
|
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(patched.pNext));
|
|
|
|
UnwrapNextChain(m_State, "VkMemoryAllocateInfo", tempMem, (VkBaseInStructure *)&patched);
|
|
|
|
if(patched.memoryTypeIndex >= m_PhysicalDeviceData.memProps.memoryTypeCount)
|
|
{
|
|
RDCERR(
|
|
"Tried to allocate memory from index %u, but on replay we only have %u memory types.\n"
|
|
"This is most likely caused by incompatible hardware or drivers between capture and "
|
|
"replay, causing a change in memory requirements.",
|
|
patched.memoryTypeIndex, m_PhysicalDeviceData.memProps.memoryTypeCount);
|
|
return false;
|
|
}
|
|
|
|
VkResult ret = ObjDisp(device)->AllocateMemory(Unwrap(device), &patched, NULL, &mem);
|
|
|
|
if(ret != VK_SUCCESS)
|
|
{
|
|
RDCERR("Failed on resource serialise-creation, VkResult: %s", ToStr(ret).c_str());
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ResourceId live = GetResourceManager()->WrapResource(Unwrap(device), mem);
|
|
GetResourceManager()->AddLiveResource(Memory, mem);
|
|
|
|
m_CreationInfo.m_Memory[live].Init(GetResourceManager(), m_CreationInfo, &AllocateInfo);
|
|
|
|
VkMemoryDedicatedAllocateInfo *dedicated = (VkMemoryDedicatedAllocateInfo *)FindNextStruct(
|
|
&AllocateInfo, VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO);
|
|
if(dedicated && dedicated->buffer == VK_NULL_HANDLE && dedicated->image == VK_NULL_HANDLE)
|
|
{
|
|
dedicated = NULL;
|
|
}
|
|
|
|
VkDedicatedAllocationMemoryAllocateInfoNV *dedicatedNV =
|
|
(VkDedicatedAllocationMemoryAllocateInfoNV *)FindNextStruct(
|
|
&AllocateInfo, VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_MEMORY_ALLOCATE_INFO_NV);
|
|
if(dedicatedNV && dedicatedNV->buffer == VK_NULL_HANDLE && dedicatedNV->image == VK_NULL_HANDLE)
|
|
{
|
|
dedicatedNV = NULL;
|
|
}
|
|
|
|
if(dedicated)
|
|
{
|
|
// either set the buffer that's dedicated, or if this is dedicated image memory set NULL
|
|
m_CreationInfo.m_Memory[live].wholeMemBuf = dedicated->buffer;
|
|
|
|
uint64_t bufSize = m_CreationInfo.m_Buffer[GetResID(dedicated->buffer)].size;
|
|
uint64_t &memSize = m_CreationInfo.m_Memory[live].wholeMemBufSize;
|
|
if(memSize > bufSize)
|
|
{
|
|
RDCDEBUG("Truncating memory size %llu to dedicated buffer size %llu for %s", memSize,
|
|
bufSize, ToStr(Memory).c_str());
|
|
memSize = bufSize;
|
|
}
|
|
}
|
|
else if(dedicatedNV)
|
|
{
|
|
m_CreationInfo.m_Memory[live].wholeMemBuf = dedicatedNV->buffer;
|
|
|
|
uint64_t bufSize = m_CreationInfo.m_Buffer[GetResID(dedicatedNV->buffer)].size;
|
|
uint64_t &memSize = m_CreationInfo.m_Memory[live].wholeMemBufSize;
|
|
if(memSize > bufSize)
|
|
{
|
|
RDCDEBUG("Truncating memory size %llu to dedicated buffer size %llu for %s", memSize,
|
|
bufSize, ToStr(Memory).c_str());
|
|
memSize = bufSize;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// create a buffer with the whole memory range bound, for copying to and from
|
|
// conveniently (for initial state data)
|
|
VkBuffer buf = VK_NULL_HANDLE;
|
|
|
|
VkBufferCreateInfo bufInfo = {
|
|
VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
|
NULL,
|
|
0,
|
|
AllocateInfo.allocationSize,
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
};
|
|
|
|
ret = ObjDisp(device)->CreateBuffer(Unwrap(device), &bufInfo, NULL, &buf);
|
|
RDCASSERTEQUAL(ret, VK_SUCCESS);
|
|
|
|
// we already validated at replay time that the memory size is aligned/etc as necessary so
|
|
// we can create a buffer of the whole size, but just to keep the validation layers happy
|
|
// let's check the requirements here again.
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), buf, &mrq);
|
|
|
|
// check that this allocation type can actually be bound to a buffer. Allocations that can't
|
|
// be used with buffers we can just skip and leave wholeMemBuf as NULL.
|
|
if((1 << AllocateInfo.memoryTypeIndex) & mrq.memoryTypeBits)
|
|
{
|
|
RDCASSERT(mrq.size <= AllocateInfo.allocationSize, mrq.size, AllocateInfo.allocationSize);
|
|
|
|
ResourceId bufid = GetResourceManager()->WrapResource(Unwrap(device), buf);
|
|
|
|
ObjDisp(device)->BindBufferMemory(Unwrap(device), Unwrap(buf), Unwrap(mem), 0);
|
|
|
|
// register as a live-only resource, so it is cleaned up properly
|
|
GetResourceManager()->AddLiveResource(bufid, buf);
|
|
|
|
m_CreationInfo.m_Memory[live].wholeMemBuf = buf;
|
|
}
|
|
else
|
|
{
|
|
RDCWARN("Can't create buffer covering memory allocation %s", ToStr(Memory).c_str());
|
|
ObjDisp(device)->DestroyBuffer(Unwrap(device), buf, NULL);
|
|
|
|
m_CreationInfo.m_Memory[live].wholeMemBuf = VK_NULL_HANDLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
AddResource(Memory, ResourceType::Memory, "Memory");
|
|
DerivedResource(device, Memory);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo *pAllocateInfo,
|
|
const VkAllocationCallbacks *pAllocator,
|
|
VkDeviceMemory *pMemory)
|
|
{
|
|
VkMemoryAllocateInfo info = *pAllocateInfo;
|
|
|
|
{
|
|
// we need to be able to allocate a buffer that covers the whole memory range. However
|
|
// if the memory is e.g. 100 bytes (arbitrary example) and buffers have memory requirements
|
|
// such that it must be bound to a multiple of 128 bytes, then we can't create a buffer
|
|
// that entirely covers a 100 byte allocation.
|
|
// To get around this, we create a buffer of the allocation's size with the properties we
|
|
// want, check its required size, then bump up the allocation size to that as if the application
|
|
// had requested more. We're assuming here no system will require something like "buffer of
|
|
// size N must be bound to memory of size N+O for some value of O overhead bytes".
|
|
//
|
|
// this could be optimised as maybe we'll be creating buffers of multiple sizes, but allocation
|
|
// in vulkan is already expensive and making it a little more expensive isn't a big deal.
|
|
|
|
VkBufferCreateInfo bufInfo = {
|
|
VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
|
NULL,
|
|
0,
|
|
info.allocationSize,
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
};
|
|
|
|
// since this is very short lived, it's not wrapped
|
|
VkBuffer buf;
|
|
|
|
VkResult vkr = ObjDisp(device)->CreateBuffer(Unwrap(device), &bufInfo, NULL, &buf);
|
|
CheckVkResult(vkr);
|
|
|
|
if(vkr == VK_SUCCESS && buf != VK_NULL_HANDLE)
|
|
{
|
|
VkMemoryRequirements mrq = {0};
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), buf, &mrq);
|
|
|
|
RDCASSERTMSG("memory requirements less than desired size", mrq.size >= bufInfo.size, mrq.size,
|
|
bufInfo.size);
|
|
|
|
// round up allocation size to allow creation of buffers
|
|
if(mrq.size >= bufInfo.size)
|
|
info.allocationSize = mrq.size;
|
|
}
|
|
|
|
ObjDisp(device)->DestroyBuffer(Unwrap(device), buf, NULL);
|
|
}
|
|
|
|
VkMemoryAllocateInfo unwrapped = info;
|
|
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(unwrapped.pNext));
|
|
|
|
UnwrapNextChain(m_State, "VkMemoryAllocateInfo", tempMem, (VkBaseInStructure *)&unwrapped);
|
|
|
|
VkMemoryAllocateFlagsInfo *memFlags = (VkMemoryAllocateFlagsInfo *)FindNextStruct(
|
|
&unwrapped, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO);
|
|
|
|
// since the application must specify VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT itself, we can
|
|
// assume the struct is present and just add the capture-replay flag to allow us to specify the
|
|
// address on replay. We ensured the physical device can support this feature (and it was enabled)
|
|
// when whitelisting the extension and creating the device.
|
|
if(IsCaptureMode(m_State) && memFlags && (memFlags->flags & VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT))
|
|
memFlags->flags |= VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT;
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->AllocateMemory(Unwrap(device), &unwrapped, pAllocator, pMemory));
|
|
|
|
// restore the memoryTypeIndex to the original, as that's what we want to serialise,
|
|
// but maintain any potential modifications we made to info.allocationSize
|
|
info.memoryTypeIndex = pAllocateInfo->memoryTypeIndex;
|
|
|
|
if(ret == VK_SUCCESS)
|
|
{
|
|
ResourceId id = GetResourceManager()->WrapResource(Unwrap(device), *pMemory);
|
|
|
|
VkMemoryDedicatedAllocateInfo *dedicated = (VkMemoryDedicatedAllocateInfo *)FindNextStruct(
|
|
pAllocateInfo, VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO);
|
|
if(dedicated && dedicated->buffer == VK_NULL_HANDLE && dedicated->image == VK_NULL_HANDLE)
|
|
{
|
|
dedicated = NULL;
|
|
}
|
|
|
|
VkDedicatedAllocationMemoryAllocateInfoNV *dedicatedNV =
|
|
(VkDedicatedAllocationMemoryAllocateInfoNV *)FindNextStruct(
|
|
pAllocateInfo, VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_MEMORY_ALLOCATE_INFO_NV);
|
|
if(dedicatedNV && dedicatedNV->buffer == VK_NULL_HANDLE && dedicatedNV->image == VK_NULL_HANDLE)
|
|
{
|
|
dedicatedNV = NULL;
|
|
}
|
|
|
|
// create a buffer with the whole memory range bound, for copying to and from
|
|
// conveniently (for initial state data)
|
|
VkBuffer wholeMemBuf = VK_NULL_HANDLE;
|
|
|
|
VkBufferCreateInfo bufInfo = {
|
|
VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
|
NULL,
|
|
0,
|
|
info.allocationSize,
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
};
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
// we make the buffer concurrently accessible by all queue families to not invalidate the
|
|
// contents of the memory we're reading back from.
|
|
bufInfo.sharingMode = VK_SHARING_MODE_CONCURRENT;
|
|
bufInfo.queueFamilyIndexCount = (uint32_t)m_QueueFamilyIndices.size();
|
|
bufInfo.pQueueFamilyIndices = m_QueueFamilyIndices.data();
|
|
|
|
// spec requires that CONCURRENT must specify more than one queue family. If there is only one
|
|
// queue family, we can safely use exclusive.
|
|
if(bufInfo.queueFamilyIndexCount == 1)
|
|
bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
}
|
|
|
|
VkDeviceSize memSize = info.allocationSize;
|
|
ResourceId bufid;
|
|
|
|
if(dedicated)
|
|
{
|
|
// either set the buffer that's dedicated, or if this is dedicated image memory set NULL
|
|
wholeMemBuf = dedicated->buffer;
|
|
}
|
|
else if(dedicatedNV)
|
|
{
|
|
wholeMemBuf = dedicatedNV->buffer;
|
|
}
|
|
else
|
|
{
|
|
ret = ObjDisp(device)->CreateBuffer(Unwrap(device), &bufInfo, NULL, &wholeMemBuf);
|
|
RDCASSERTEQUAL(ret, VK_SUCCESS);
|
|
|
|
// we already validated above that the memory size is aligned/etc as necessary so we can
|
|
// create a buffer of the whole size, but just to keep the validation layers happy let's check
|
|
// the requirements here again.
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), wholeMemBuf, &mrq);
|
|
|
|
RDCASSERTEQUAL(mrq.size, info.allocationSize);
|
|
|
|
if((mrq.memoryTypeBits & (1U << info.memoryTypeIndex)) != 0)
|
|
{
|
|
bufid = GetResourceManager()->WrapResource(Unwrap(device), wholeMemBuf);
|
|
|
|
ObjDisp(device)->BindBufferMemory(Unwrap(device), Unwrap(wholeMemBuf), Unwrap(*pMemory), 0);
|
|
}
|
|
else
|
|
{
|
|
// can't create a memory-spanning buffer for this allocation. Assume this is a case where
|
|
// this memory type is only available to images and is not mappable - in which case the
|
|
// whole memory buffer won't be needed so we can skip this.
|
|
ObjDisp(device)->DestroyBuffer(Unwrap(device), wholeMemBuf, NULL);
|
|
wholeMemBuf = VK_NULL_HANDLE;
|
|
}
|
|
}
|
|
|
|
if((dedicated != NULL || dedicatedNV != NULL) && wholeMemBuf != VK_NULL_HANDLE)
|
|
{
|
|
VkDeviceSize bufSize = IsCaptureMode(m_State)
|
|
? GetRecord(wholeMemBuf)->memSize
|
|
: m_CreationInfo.m_Buffer[GetResID(wholeMemBuf)].size;
|
|
if(memSize > bufSize)
|
|
{
|
|
RDCDEBUG("Truncating memory size %llu to dedicated buffer size %llu for %s", memSize,
|
|
bufSize, ToStr(id).c_str());
|
|
memSize = bufSize;
|
|
}
|
|
}
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
VkMemoryAllocateInfo serialisedInfo = info;
|
|
VkMemoryOpaqueCaptureAddressAllocateInfo memoryDeviceAddress = {
|
|
VK_STRUCTURE_TYPE_MEMORY_OPAQUE_CAPTURE_ADDRESS_ALLOCATE_INFO,
|
|
};
|
|
|
|
// create resource record for gpu memory
|
|
VkResourceRecord *record = GetResourceManager()->AddResourceRecord(*pMemory);
|
|
RDCASSERT(record);
|
|
|
|
memFlags = (VkMemoryAllocateFlagsInfo *)FindNextStruct(
|
|
&serialisedInfo, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO);
|
|
|
|
if(memFlags && (memFlags->flags & VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT))
|
|
{
|
|
VkDeviceMemoryOpaqueCaptureAddressInfo getInfo = {
|
|
VK_STRUCTURE_TYPE_DEVICE_MEMORY_OPAQUE_CAPTURE_ADDRESS_INFO, NULL, Unwrap(*pMemory),
|
|
};
|
|
|
|
memoryDeviceAddress.opaqueCaptureAddress =
|
|
ObjDisp(device)->GetDeviceMemoryOpaqueCaptureAddress(Unwrap(device), &getInfo);
|
|
|
|
// we explicitly DON'T assert on this, because some drivers will only need the device
|
|
// address specified at allocate time.
|
|
// RDCASSERT(memoryDeviceAddress.opaqueCaptureAddress);
|
|
|
|
// push this struct onto the start of the chain
|
|
memoryDeviceAddress.pNext = serialisedInfo.pNext;
|
|
serialisedInfo.pNext = &memoryDeviceAddress;
|
|
|
|
memFlags->flags |= VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT;
|
|
|
|
{
|
|
SCOPED_READLOCK(m_CapTransitionLock);
|
|
m_DeviceAddressResources.IDs.push_back(record->GetResourceID());
|
|
}
|
|
}
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkAllocateMemory);
|
|
Serialise_vkAllocateMemory(ser, device, &serialisedInfo, NULL, pMemory);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
record->AddChunk(chunk);
|
|
|
|
record->Length = memSize;
|
|
|
|
uint32_t memProps =
|
|
m_PhysicalDeviceData.memProps.memoryTypes[info.memoryTypeIndex].propertyFlags;
|
|
|
|
record->memMapState = new MemMapState();
|
|
record->memMapState->wholeMemBuf = wholeMemBuf;
|
|
record->memMapState->dedicated = dedicated != NULL || dedicatedNV != NULL;
|
|
|
|
// if memory is not host visible, so not mappable, don't create map state at all
|
|
if((memProps & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0)
|
|
{
|
|
record->memMapState->mapCoherent = (memProps & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
|
|
|
|
// only mark this memory as needing readback on the GPU if it's device-local on a discrete
|
|
// GPU. On non-discrete GPUs we assume the CPU can access the memory at a good speed, and on
|
|
// discrete GPUs where the memory isn't device local it's CPU side so of course it's fast to
|
|
// access. Only in this case do we want to push the memory from the GPU to the CPU with a
|
|
// command buffer.
|
|
if(Vulkan_GPUReadbackDeviceLocal() &&
|
|
m_PhysicalDeviceData.props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
|
|
{
|
|
record->memMapState->readbackOnGPU =
|
|
((memProps & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0);
|
|
|
|
// we need a wholeMemBuf to readback on the GPU
|
|
if(record->memMapState->readbackOnGPU && wholeMemBuf == VK_NULL_HANDLE)
|
|
{
|
|
RDCWARN(
|
|
"Memory allocation would have been readback on GPU, but can't without wholeMemBuf");
|
|
record->memMapState->readbackOnGPU = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
GetResourceManager()->AddDeviceMemory(id);
|
|
}
|
|
else
|
|
{
|
|
GetResourceManager()->AddLiveResource(id, *pMemory);
|
|
|
|
m_CreationInfo.m_Memory[id].Init(GetResourceManager(), m_CreationInfo, &info);
|
|
|
|
if(dedicated == NULL && dedicatedNV == NULL && wholeMemBuf != VK_NULL_HANDLE)
|
|
{
|
|
// register as a live-only resource, so it is cleaned up properly
|
|
GetResourceManager()->AddLiveResource(bufid, wholeMemBuf);
|
|
}
|
|
|
|
m_CreationInfo.m_Memory[id].wholeMemBuf = wholeMemBuf;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CheckVkResult(ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void WrappedVulkan::vkFreeMemory(VkDevice device, VkDeviceMemory memory,
|
|
const VkAllocationCallbacks *pAllocator)
|
|
{
|
|
if(memory == VK_NULL_HANDLE)
|
|
return;
|
|
|
|
// we just need to clean up after ourselves on replay
|
|
WrappedVkNonDispRes *wrapped = (WrappedVkNonDispRes *)GetWrapped(memory);
|
|
|
|
VkDeviceMemory unwrappedMem = wrapped->real.As<VkDeviceMemory>();
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
// artificially extend the lifespan of buffer device address memory or buffers, to ensure their
|
|
// opaque capture address isn't re-used before the capture completes
|
|
{
|
|
SCOPED_READLOCK(m_CapTransitionLock);
|
|
if(IsActiveCapturing(m_State) && m_DeviceAddressResources.IDs.contains(GetResID(memory)))
|
|
{
|
|
// we can't hold onto the user callback so we'll be freeing with NULL.
|
|
RDCASSERT(pAllocator == NULL);
|
|
m_DeviceAddressResources.DeadMemories.push_back(memory);
|
|
return;
|
|
}
|
|
m_DeviceAddressResources.IDs.removeOne(GetResID(memory));
|
|
}
|
|
|
|
MemMapState *memMapState = wrapped->record->memMapState;
|
|
|
|
if(memMapState)
|
|
{
|
|
// there is an implicit unmap on free, so make sure to tidy up
|
|
if(memMapState->refData)
|
|
{
|
|
FreeAlignedBuffer(memMapState->refData);
|
|
memMapState->refData = NULL;
|
|
}
|
|
|
|
// destroy the wholeMemBuf if it's one we allocated ourselves
|
|
if(!memMapState->dedicated)
|
|
{
|
|
ObjDisp(device)->DestroyBuffer(Unwrap(device), Unwrap(memMapState->wholeMemBuf), NULL);
|
|
GetResourceManager()->ReleaseWrappedResource(memMapState->wholeMemBuf);
|
|
}
|
|
}
|
|
|
|
{
|
|
SCOPED_LOCK(m_CoherentMapsLock);
|
|
m_CoherentMaps.removeOne(wrapped->record);
|
|
}
|
|
|
|
GetResourceManager()->RemoveDeviceMemory(wrapped->id);
|
|
}
|
|
|
|
m_CreationInfo.erase(GetResID(memory));
|
|
|
|
GetResourceManager()->ReleaseWrappedResource(memory);
|
|
|
|
ObjDisp(device)->FreeMemory(Unwrap(device), unwrappedMem, pAllocator);
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkMapMemory(VkDevice device, VkDeviceMemory mem, VkDeviceSize offset,
|
|
VkDeviceSize size, VkMemoryMapFlags flags, void **ppData)
|
|
{
|
|
// ensure we always map on a 16-byte boundary. This is for our own purposes so we can
|
|
// FindDiffRange against the mapped region. We adjust the pointer returned to the user but
|
|
// otherwise we act as if the mapped region was 16-byte aligned. Fortunately flushed regions in
|
|
// vkFlushMappedMemoryRanges are relative to the memory base, not the mapped region, so this
|
|
// offset effectively only modifies the returned pointer and has no other side-effects.
|
|
VkDeviceSize misalignedOffset = offset & 0xf;
|
|
offset &= ~0xf;
|
|
// need to adjust the size so the end-point is still the same!
|
|
size += misalignedOffset;
|
|
|
|
byte *realData = NULL;
|
|
VkResult ret = ObjDisp(device)->MapMemory(Unwrap(device), Unwrap(mem), offset, size, flags,
|
|
(void **)&realData);
|
|
|
|
if(ret == VK_SUCCESS && realData)
|
|
{
|
|
ResourceId id = GetResID(mem);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
VkResourceRecord *memrecord = GetRecord(mem);
|
|
|
|
// must have map state, only non host visible memories have no map
|
|
// state, and they can't be mapped!
|
|
RDCASSERT(memrecord->memMapState);
|
|
MemMapState &state = *memrecord->memMapState;
|
|
|
|
// ensure size is valid
|
|
RDCASSERT(size == VK_WHOLE_SIZE || (size > 0 && offset + size <= memrecord->Length),
|
|
GetResID(mem), size, memrecord->Length);
|
|
|
|
// flush range offsets are relative to the start of the memory so keep mappedPtr at that
|
|
// basis. We'll only access within the mapped range
|
|
state.cpuReadPtr = state.mappedPtr = (byte *)realData - (size_t)offset;
|
|
state.refData = NULL;
|
|
|
|
state.mapOffset = offset;
|
|
state.mapSize = size == VK_WHOLE_SIZE ? (memrecord->Length - offset)
|
|
: RDCMIN(memrecord->Length - offset, size);
|
|
|
|
*ppData = realData + misalignedOffset;
|
|
|
|
if(state.mapCoherent)
|
|
{
|
|
SCOPED_LOCK(m_CoherentMapsLock);
|
|
m_CoherentMaps.push_back(memrecord);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*ppData = realData + misalignedOffset;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*ppData = NULL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkUnmapMemory(SerialiserType &ser, VkDevice device,
|
|
VkDeviceMemory memory)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT(memory).Important();
|
|
|
|
uint64_t MapOffset = 0;
|
|
uint64_t MapSize = 0;
|
|
byte *MapData = NULL;
|
|
|
|
MemMapState *state = NULL;
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
state = GetRecord(memory)->memMapState;
|
|
|
|
MapOffset = state->mapOffset;
|
|
MapSize = state->mapSize;
|
|
|
|
MapData = (byte *)state->cpuReadPtr + MapOffset;
|
|
}
|
|
|
|
SERIALISE_ELEMENT(MapOffset);
|
|
SERIALISE_ELEMENT(MapSize);
|
|
|
|
bool directStream = true;
|
|
|
|
if(IsReplayingAndReading() && memory != VK_NULL_HANDLE)
|
|
{
|
|
if(IsLoading(m_State))
|
|
m_ResourceUses[GetResID(memory)].push_back(EventUsage(m_RootEventID, ResourceUsage::CPUWrite));
|
|
|
|
VkResult vkr = ObjDisp(device)->MapMemory(Unwrap(device), Unwrap(memory), MapOffset, MapSize, 0,
|
|
(void **)&MapData);
|
|
if(vkr != VK_SUCCESS)
|
|
{
|
|
RDCERR("Error mapping memory on replay: %s", ToStr(vkr).c_str());
|
|
return false;
|
|
}
|
|
|
|
const Intervals<VulkanCreationInfo::Memory::MemoryBinding> &bindings =
|
|
m_CreationInfo.m_Memory[GetResID(memory)].bindings;
|
|
|
|
uint64_t finish = MapOffset + MapSize;
|
|
|
|
auto it = bindings.find(MapOffset);
|
|
|
|
// iterate the bindings that this map region overlaps, if we overlap with any tiled memory we
|
|
// need to take the slow path
|
|
while(it != bindings.end() && it->start() < finish)
|
|
{
|
|
if(it->value() == VulkanCreationInfo::Memory::Tiled)
|
|
{
|
|
if(IsLoading(m_State))
|
|
{
|
|
AddDebugMessage(MessageCategory::Performance, MessageSeverity::Medium,
|
|
MessageSource::GeneralPerformance,
|
|
"Unmapped memory overlaps tiled-only memory region. "
|
|
"Taking slow path to mask tiled memory writes");
|
|
}
|
|
directStream = false;
|
|
m_MaskedMapData.resize((size_t)MapSize);
|
|
break;
|
|
}
|
|
|
|
it++;
|
|
}
|
|
}
|
|
|
|
if(directStream)
|
|
{
|
|
// not using SERIALISE_ELEMENT_ARRAY so we can deliberately avoid allocation - we serialise
|
|
// directly into upload memory
|
|
ser.Serialise("MapData"_lit, MapData, MapSize, SerialiserFlags::NoFlags).Important();
|
|
}
|
|
else
|
|
{
|
|
// serialise into temp storage
|
|
byte *tmp = m_MaskedMapData.data();
|
|
ser.Serialise("MapData"_lit, tmp, MapSize, SerialiserFlags::NoFlags).Important();
|
|
|
|
const Intervals<VulkanCreationInfo::Memory::MemoryBinding> &bindings =
|
|
m_CreationInfo.m_Memory[GetResID(memory)].bindings;
|
|
|
|
uint64_t finish = MapOffset + MapSize;
|
|
|
|
auto it = bindings.find(MapOffset);
|
|
|
|
// iterate the bindings that this map region overlaps, and only memcpy the bits that we overlap
|
|
// which are linear
|
|
while(it != bindings.end() && it->start() < finish)
|
|
{
|
|
if(it->value() != VulkanCreationInfo::Memory::Tiled)
|
|
{
|
|
// start at the map offset or the region offset, whichever is *later*. E.g. if the region is
|
|
// larger than the map we only start where the map started, and vice-versa if the map
|
|
// started earlier than the region.
|
|
// We also rebase it so that it's relative to the map, so it's the byte offset for the
|
|
// memcpy
|
|
size_t offs = size_t(RDCMAX(it->start(), MapOffset) - MapOffset);
|
|
|
|
// similarly, only copy up to the end of the region or the end ofthe map whichever is
|
|
// *sooner*.
|
|
size_t size = size_t(RDCMIN(it->finish(), finish) - offs);
|
|
|
|
memcpy(MapData + offs, m_MaskedMapData.data() + offs, size);
|
|
}
|
|
|
|
it++;
|
|
}
|
|
}
|
|
|
|
if(IsReplayingAndReading() && MapData && memory != VK_NULL_HANDLE)
|
|
ObjDisp(device)->UnmapMemory(Unwrap(device), Unwrap(memory));
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
return true;
|
|
}
|
|
|
|
void WrappedVulkan::vkUnmapMemory(VkDevice device, VkDeviceMemory mem)
|
|
{
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
ResourceId id = GetResID(mem);
|
|
|
|
VkResourceRecord *memrecord = GetRecord(mem);
|
|
|
|
RDCASSERT(memrecord->memMapState);
|
|
MemMapState &state = *memrecord->memMapState;
|
|
|
|
if(state.mapCoherent)
|
|
{
|
|
SCOPED_LOCK(m_CoherentMapsLock);
|
|
|
|
int32_t idx = m_CoherentMaps.indexOf(memrecord);
|
|
if(idx < 0)
|
|
RDCERR("vkUnmapMemory for memory handle that's not currently mapped");
|
|
else
|
|
m_CoherentMaps.erase(idx);
|
|
}
|
|
|
|
{
|
|
// decide atomically if this chunk should be in-frame or not
|
|
// so that we're not in the else branch but haven't marked
|
|
// dirty when capframe starts, then we mark dirty while in-frame
|
|
|
|
bool capframe = false;
|
|
{
|
|
SCOPED_READLOCK(m_CapTransitionLock);
|
|
capframe = IsActiveCapturing(m_State);
|
|
|
|
if(!capframe)
|
|
{
|
|
GetResourceManager()->MarkResourceFrameReferenced(id, eFrameRef_PartialWrite);
|
|
}
|
|
}
|
|
|
|
SCOPED_LOCK(state.mrLock);
|
|
|
|
if(capframe)
|
|
{
|
|
// coherent maps must always serialise all data on unmap, even if a flush was seen, because
|
|
// unflushed data is *also* visible. This is a bit redundant since data is serialised here
|
|
// and in any flushes, but that's the app's fault - the spec calls out flushing coherent
|
|
// maps
|
|
// as inefficient
|
|
// if the memory is not coherent, we must have a flush for every region written while it is
|
|
// mapped, there is no implicit flush on unmap, so we follow the spec strictly on this.
|
|
if(state.mapCoherent)
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkUnmapMemory);
|
|
Serialise_vkUnmapMemory(ser, device, mem);
|
|
|
|
VkResourceRecord *record = GetRecord(mem);
|
|
|
|
if(IsBackgroundCapturing(m_State))
|
|
{
|
|
record->AddChunk(scope.Get());
|
|
}
|
|
else
|
|
{
|
|
m_FrameCaptureRecord->AddChunk(scope.Get());
|
|
GetResourceManager()->MarkMemoryFrameReferenced(id, state.mapOffset, state.mapSize,
|
|
eFrameRef_PartialWrite);
|
|
}
|
|
}
|
|
}
|
|
|
|
state.cpuReadPtr = state.mappedPtr = NULL;
|
|
}
|
|
|
|
FreeAlignedBuffer(state.refData);
|
|
state.refData = NULL;
|
|
}
|
|
|
|
ObjDisp(device)->UnmapMemory(Unwrap(device), Unwrap(mem));
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkFlushMappedMemoryRanges(SerialiserType &ser, VkDevice device,
|
|
uint32_t memRangeCount,
|
|
const VkMappedMemoryRange *pMemRanges)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT(memRangeCount);
|
|
SERIALISE_ELEMENT_LOCAL(MemRange, *pMemRanges).Important();
|
|
|
|
byte *MappedData = NULL;
|
|
uint64_t memRangeSize = 1;
|
|
|
|
MemMapState *state = NULL;
|
|
if(ser.IsWriting())
|
|
{
|
|
VkResourceRecord *record = GetRecord(MemRange.memory);
|
|
state = record->memMapState;
|
|
|
|
memRangeSize = MemRange.size;
|
|
if(memRangeSize == VK_WHOLE_SIZE)
|
|
memRangeSize = record->Length - MemRange.offset;
|
|
|
|
// don't support any extensions on VkMappedMemoryRange
|
|
RDCASSERT(pMemRanges->pNext == NULL);
|
|
|
|
MappedData = state->cpuReadPtr + (size_t)MemRange.offset;
|
|
}
|
|
|
|
bool directStream = true;
|
|
|
|
if(IsReplayingAndReading() && MemRange.memory != VK_NULL_HANDLE && MemRange.size > 0)
|
|
{
|
|
if(IsLoading(m_State))
|
|
m_ResourceUses[GetResID(MemRange.memory)].push_back(
|
|
EventUsage(m_RootEventID, ResourceUsage::CPUWrite));
|
|
|
|
VkResult ret =
|
|
ObjDisp(device)->MapMemory(Unwrap(device), Unwrap(MemRange.memory), MemRange.offset,
|
|
MemRange.size, 0, (void **)&MappedData);
|
|
if(ret != VK_SUCCESS)
|
|
RDCERR("Error mapping memory on replay: %s", ToStr(ret).c_str());
|
|
|
|
const VulkanCreationInfo::Memory &memInfo = m_CreationInfo.m_Memory[GetResID(MemRange.memory)];
|
|
const Intervals<VulkanCreationInfo::Memory::MemoryBinding> &bindings = memInfo.bindings;
|
|
|
|
memRangeSize = MemRange.size;
|
|
if(memRangeSize == VK_WHOLE_SIZE)
|
|
memRangeSize = memInfo.allocSize - MemRange.offset;
|
|
|
|
uint64_t finish = MemRange.offset + memRangeSize;
|
|
|
|
auto it = bindings.find(MemRange.offset);
|
|
|
|
// iterate the bindings that this map region overlaps, if we overlap with any tiled memory we
|
|
// need to take the slow path
|
|
while(it != bindings.end() && it->start() < finish)
|
|
{
|
|
if(it->value() == VulkanCreationInfo::Memory::Tiled)
|
|
{
|
|
if(IsLoading(m_State))
|
|
{
|
|
AddDebugMessage(MessageCategory::Performance, MessageSeverity::Medium,
|
|
MessageSource::GeneralPerformance,
|
|
"Unmapped memory overlaps tiled-only memory region. "
|
|
"Taking slow path to mask tiled memory writes");
|
|
}
|
|
directStream = false;
|
|
m_MaskedMapData.resize((size_t)memRangeSize);
|
|
break;
|
|
}
|
|
|
|
it++;
|
|
}
|
|
}
|
|
|
|
if(directStream)
|
|
{
|
|
// not using SERIALISE_ELEMENT_ARRAY so we can deliberately avoid allocation - we serialise
|
|
// directly into upload memory
|
|
ser.Serialise("MapData"_lit, MappedData, memRangeSize, SerialiserFlags::NoFlags).Important();
|
|
}
|
|
else
|
|
{
|
|
// serialise into temp storage
|
|
byte *tmp = m_MaskedMapData.data();
|
|
ser.Serialise("MapData"_lit, tmp, memRangeSize, SerialiserFlags::NoFlags).Important();
|
|
|
|
const Intervals<VulkanCreationInfo::Memory::MemoryBinding> &bindings =
|
|
m_CreationInfo.m_Memory[GetResID(MemRange.memory)].bindings;
|
|
|
|
uint64_t mappedRegionStart = MemRange.offset;
|
|
uint64_t mappedRegionFinish = MemRange.offset + memRangeSize;
|
|
|
|
auto it = bindings.find(mappedRegionStart);
|
|
|
|
// iterate the bindings that this map region overlaps, and only memcpy the bits that we overlap
|
|
// which are linear
|
|
while(it != bindings.end() && it->start() < mappedRegionFinish)
|
|
{
|
|
if(it->value() != VulkanCreationInfo::Memory::Tiled)
|
|
{
|
|
// start at the map offset or the region offset, whichever is *later*. E.g. if the region is
|
|
// larger than the map we only start where the map started, and vice-versa if the map
|
|
// started earlier than the region.
|
|
uint64_t start = RDCMAX(it->start(), mappedRegionStart);
|
|
|
|
// similarly, finish at the end of the region or the end of the map whichever is *sooner*.
|
|
uint64_t finish = RDCMIN(it->finish(), mappedRegionFinish);
|
|
|
|
// Transform now to be relative to the start of the map. Note that since we max'd with
|
|
// the map start/finish above this won't underflow
|
|
size_t offs = size_t(start - mappedRegionStart);
|
|
size_t size = size_t(finish - start);
|
|
|
|
memcpy(MappedData + offs, m_MaskedMapData.data() + offs, size);
|
|
}
|
|
|
|
it++;
|
|
}
|
|
}
|
|
|
|
if(IsReplayingAndReading() && MappedData && MemRange.memory != VK_NULL_HANDLE && MemRange.size > 0)
|
|
ObjDisp(device)->UnmapMemory(Unwrap(device), Unwrap(MemRange.memory));
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
// if we need to save off this serialised buffer as reference for future comparison,
|
|
// do so now. See the call to vkFlushMappedMemoryRanges in WrappedVulkan::vkQueueSubmit()
|
|
if(ser.IsWriting() && state->needRefData)
|
|
{
|
|
if(!state->refData)
|
|
{
|
|
// if we're in this case, the range should be for the whole memory region.
|
|
RDCASSERT(MemRange.offset == state->mapOffset && memRangeSize == state->mapSize,
|
|
MemRange.offset, memRangeSize, state->mapOffset, state->mapSize);
|
|
|
|
// allocate ref data so we can compare next time to minimise serialised data
|
|
state->refData = AllocAlignedBuffer((size_t)state->mapSize);
|
|
}
|
|
|
|
// the memory range offset should always be at least the map offset
|
|
RDCASSERT(MemRange.offset >= state->mapOffset, MemRange.offset, state->mapOffset);
|
|
|
|
// it's no longer safe to use state->mappedPtr, we need to save *precisely* what
|
|
// was serialised. We do this by copying out of the serialiser since we know this
|
|
// memory is not changing
|
|
size_t offs = size_t(ser.GetWriter()->GetOffset() - memRangeSize);
|
|
|
|
const byte *serialisedData = ser.GetWriter()->GetData() + offs;
|
|
|
|
memcpy(state->refData + MemRange.offset - state->mapOffset, serialisedData, (size_t)memRangeSize);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void WrappedVulkan::InternalFlushMemoryRange(VkDevice device, const VkMappedMemoryRange &memRange,
|
|
bool internalFlush, bool capframe)
|
|
{
|
|
ResourceId memid = GetResID(memRange.memory);
|
|
VkResourceRecord *record = GetRecord(memRange.memory);
|
|
|
|
MemMapState *state = record->memMapState;
|
|
|
|
if(state->mappedPtr == NULL)
|
|
{
|
|
RDCERR("Flushing memory %s that isn't currently mapped", ToStr(memid).c_str());
|
|
return;
|
|
}
|
|
|
|
if(capframe)
|
|
{
|
|
SCOPED_LOCK_OPTIONAL(state->mrLock, !internalFlush);
|
|
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(internalFlush ? VulkanChunk::CoherentMapWrite
|
|
: VulkanChunk::vkFlushMappedMemoryRanges);
|
|
Serialise_vkFlushMappedMemoryRanges(ser, device, 1, &memRange);
|
|
|
|
m_FrameCaptureRecord->AddChunk(scope.Get());
|
|
}
|
|
|
|
if(capframe)
|
|
{
|
|
VkDeviceSize offs = memRange.offset;
|
|
VkDeviceSize size = memRange.size;
|
|
|
|
// map VK_WHOLE_SIZE into a specific size
|
|
if(size == VK_WHOLE_SIZE)
|
|
size = state->mapOffset + state->mapSize - offs;
|
|
|
|
GetResourceManager()->MarkMemoryFrameReferenced(memid, offs, size, eFrameRef_CompleteWrite);
|
|
}
|
|
else
|
|
{
|
|
FrameRefType refType = eFrameRef_PartialWrite;
|
|
if(memRange.offset == 0 && memRange.size >= record->Length)
|
|
refType = eFrameRef_CompleteWrite;
|
|
|
|
GetResourceManager()->MarkResourceFrameReferenced(memid, refType);
|
|
}
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkFlushMappedMemoryRanges(VkDevice device, uint32_t memRangeCount,
|
|
const VkMappedMemoryRange *pMemRanges)
|
|
{
|
|
VkMappedMemoryRange *unwrapped = GetTempArray<VkMappedMemoryRange>(memRangeCount);
|
|
for(uint32_t i = 0; i < memRangeCount; i++)
|
|
{
|
|
unwrapped[i] = pMemRanges[i];
|
|
unwrapped[i].memory = Unwrap(unwrapped[i].memory);
|
|
}
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->FlushMappedMemoryRanges(Unwrap(device), memRangeCount, unwrapped));
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
bool capframe = false;
|
|
|
|
{
|
|
SCOPED_READLOCK(m_CapTransitionLock);
|
|
capframe = IsActiveCapturing(m_State);
|
|
}
|
|
|
|
for(uint32_t i = 0; i < memRangeCount; i++)
|
|
{
|
|
InternalFlushMemoryRange(device, pMemRanges[i], false, capframe);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkInvalidateMappedMemoryRanges(VkDevice device, uint32_t memRangeCount,
|
|
const VkMappedMemoryRange *pMemRanges)
|
|
{
|
|
VkMappedMemoryRange *unwrapped = GetTempArray<VkMappedMemoryRange>(memRangeCount);
|
|
for(uint32_t i = 0; i < memRangeCount; i++)
|
|
{
|
|
unwrapped[i] = pMemRanges[i];
|
|
unwrapped[i].memory = Unwrap(unwrapped[i].memory);
|
|
}
|
|
|
|
// don't need to serialise this, readback from mapped memory is not captured
|
|
// and is only relevant for the application.
|
|
return ObjDisp(device)->InvalidateMappedMemoryRanges(Unwrap(device), memRangeCount, unwrapped);
|
|
}
|
|
|
|
// Generic API object functions
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkBindBufferMemory(SerialiserType &ser, VkDevice device,
|
|
VkBuffer buffer, VkDeviceMemory memory,
|
|
VkDeviceSize memoryOffset)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT(buffer).Important();
|
|
SERIALISE_ELEMENT(memory).Important();
|
|
SERIALISE_ELEMENT(memoryOffset);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
ResourceId resOrigId = GetResourceManager()->GetOriginalID(GetResID(buffer));
|
|
ResourceId memOrigId = GetResourceManager()->GetOriginalID(GetResID(memory));
|
|
|
|
VulkanCreationInfo::Buffer &bufInfo = m_CreationInfo.m_Buffer[GetResID(buffer)];
|
|
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), Unwrap(buffer), &mrq);
|
|
|
|
bool ok = CheckMemoryRequirements(("Buffer " + ToStr(resOrigId)).c_str(), GetResID(memory),
|
|
memoryOffset, mrq, bufInfo.external);
|
|
|
|
if(!ok)
|
|
return false;
|
|
|
|
ObjDisp(device)->BindBufferMemory(Unwrap(device), Unwrap(buffer), Unwrap(memory), memoryOffset);
|
|
|
|
GetResourceDesc(memOrigId).derivedResources.push_back(resOrigId);
|
|
GetResourceDesc(resOrigId).parentResources.push_back(memOrigId);
|
|
|
|
AddResourceCurChunk(memOrigId);
|
|
AddResourceCurChunk(resOrigId);
|
|
|
|
// for buffers created with device addresses, fetch it now as that's possible for both EXT and
|
|
// KHR variants now.
|
|
if(bufInfo.usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT)
|
|
{
|
|
VkBufferDeviceAddressInfo getInfo = {
|
|
VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, NULL, Unwrap(buffer),
|
|
};
|
|
|
|
RDCCOMPILE_ASSERT(VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO ==
|
|
VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO_EXT,
|
|
"KHR and EXT buffer_device_address should be interchangeable here.");
|
|
|
|
if(GetExtensions(GetRecord(device)).ext_KHR_buffer_device_address)
|
|
bufInfo.gpuAddress = ObjDisp(device)->GetBufferDeviceAddress(Unwrap(device), &getInfo);
|
|
else if(GetExtensions(GetRecord(device)).ext_EXT_buffer_device_address)
|
|
bufInfo.gpuAddress = ObjDisp(device)->GetBufferDeviceAddressEXT(Unwrap(device), &getInfo);
|
|
}
|
|
|
|
m_CreationInfo.m_Memory[GetResID(memory)].BindMemory(memoryOffset, mrq.size,
|
|
VulkanCreationInfo::Memory::Linear);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory,
|
|
VkDeviceSize memoryOffset)
|
|
{
|
|
VkResourceRecord *record = GetRecord(buffer);
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(ret = ObjDisp(device)->BindBufferMemory(Unwrap(device), Unwrap(buffer),
|
|
Unwrap(memory), memoryOffset));
|
|
|
|
CheckVkResult(ret);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkBindBufferMemory);
|
|
Serialise_vkBindBufferMemory(ser, device, buffer, memory, memoryOffset);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
ResourceId id = GetResID(memory);
|
|
|
|
// memory object bindings are immutable and must happen before creation or use,
|
|
// so this can always go into the record, even if a resource is created and bound
|
|
// to memory mid-frame
|
|
record->AddChunk(chunk);
|
|
|
|
VkResourceRecord *memrecord = GetRecord(memory);
|
|
|
|
record->AddParent(memrecord);
|
|
record->baseResource = id;
|
|
record->dedicated = memrecord->memMapState->dedicated;
|
|
record->memOffset = memoryOffset;
|
|
|
|
memrecord->storable |= record->storable;
|
|
|
|
// if the buffer was force-referenced, do the same with the memory
|
|
if(IsForcedReference(record))
|
|
{
|
|
// in case we're currently capturing, immediately consider the buffer and backing memory as
|
|
// read-before-write referenced
|
|
GetResourceManager()->MarkResourceFrameReferenced(record->GetResourceID(),
|
|
eFrameRef_ReadBeforeWrite);
|
|
GetResourceManager()->MarkMemoryFrameReferenced(id, memoryOffset, record->memSize,
|
|
eFrameRef_ReadBeforeWrite);
|
|
|
|
memrecord->hasBDA = true;
|
|
}
|
|
|
|
// the memory is immediately dirty because we don't use dirty tracking, it's too expensive to
|
|
// follow all frame refs in the background and it's pointless because memory almost always
|
|
// immediately becomes dirty anyway. The one case we might care about non-dirty memory is
|
|
// memory that has been allocated but not used, but that will be skipped or postponed as
|
|
// appropriate.
|
|
GetResourceManager()->MarkDirtyResource(GetResID(memory));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkBindImageMemory(SerialiserType &ser, VkDevice device, VkImage image,
|
|
VkDeviceMemory memory, VkDeviceSize memoryOffset)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT(image).Important();
|
|
SERIALISE_ELEMENT(memory).Important();
|
|
SERIALISE_ELEMENT(memoryOffset);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
ResourceId resOrigId = GetResourceManager()->GetOriginalID(GetResID(image));
|
|
ResourceId memOrigId = GetResourceManager()->GetOriginalID(GetResID(memory));
|
|
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetImageMemoryRequirements(Unwrap(device), Unwrap(image), &mrq);
|
|
|
|
VulkanCreationInfo::Image &imgInfo = m_CreationInfo.m_Image[GetResID(image)];
|
|
|
|
bool ok = CheckMemoryRequirements(("Image " + ToStr(resOrigId)).c_str(), GetResID(memory),
|
|
memoryOffset, mrq, imgInfo.external);
|
|
|
|
if(!ok)
|
|
return false;
|
|
|
|
ObjDisp(device)->BindImageMemory(Unwrap(device), Unwrap(image), Unwrap(memory), memoryOffset);
|
|
|
|
{
|
|
LockedImageStateRef state = FindImageState(GetResID(image));
|
|
if(!state)
|
|
{
|
|
RDCERR("Binding memory for unknown image %s", ToStr(GetResID(image)).c_str());
|
|
}
|
|
else
|
|
{
|
|
state->isMemoryBound = true;
|
|
state->boundMemory = GetResID(memory);
|
|
state->boundMemoryOffset = memoryOffset;
|
|
state->boundMemorySize = mrq.size;
|
|
}
|
|
}
|
|
|
|
GetResourceDesc(memOrigId).derivedResources.push_back(resOrigId);
|
|
GetResourceDesc(resOrigId).parentResources.push_back(memOrigId);
|
|
|
|
AddResourceCurChunk(memOrigId);
|
|
AddResourceCurChunk(resOrigId);
|
|
|
|
m_CreationInfo.m_Memory[GetResID(memory)].BindMemory(
|
|
memoryOffset, mrq.size,
|
|
imgInfo.linear ? VulkanCreationInfo::Memory::Linear : VulkanCreationInfo::Memory::Tiled);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkBindImageMemory(VkDevice device, VkImage image, VkDeviceMemory mem,
|
|
VkDeviceSize memOffset)
|
|
{
|
|
VkResourceRecord *record = GetRecord(image);
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(ret = ObjDisp(device)->BindImageMemory(Unwrap(device), Unwrap(image),
|
|
Unwrap(mem), memOffset));
|
|
|
|
CheckVkResult(ret);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkBindImageMemory);
|
|
Serialise_vkBindImageMemory(ser, device, image, mem, memOffset);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
{
|
|
LockedImageStateRef state = FindImageState(GetResID(image));
|
|
if(!state)
|
|
RDCERR("Binding memory to unknown image %s", ToStr(GetResID(image)).c_str());
|
|
else
|
|
state->isMemoryBound = true;
|
|
}
|
|
|
|
// memory object bindings are immutable and must happen before creation or use,
|
|
// so this can always go into the record, even if a resource is created and bound
|
|
// to memory mid-frame
|
|
record->AddChunk(chunk);
|
|
|
|
VkResourceRecord *memrecord = GetRecord(mem);
|
|
|
|
record->AddParent(memrecord);
|
|
|
|
// images are a base resource but we want to track where their memory comes from.
|
|
// Anything that looks up a baseResource for an image knows not to chase further
|
|
// than the image.
|
|
record->baseResource = memrecord->GetResourceID();
|
|
record->dedicated = memrecord->memMapState->dedicated;
|
|
}
|
|
else
|
|
{
|
|
{
|
|
LockedImageStateRef state = FindImageState(GetResID(image));
|
|
if(!state)
|
|
RDCERR("Binding memory to unknown image %s", ToStr(GetResID(image)).c_str());
|
|
else
|
|
state->isMemoryBound = true;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkCreateBuffer(SerialiserType &ser, VkDevice device,
|
|
const VkBufferCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator,
|
|
VkBuffer *pBuffer)
|
|
{
|
|
VkMemoryRequirements memoryRequirements = {};
|
|
|
|
if(ser.IsWriting())
|
|
{
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), Unwrap(*pBuffer),
|
|
&memoryRequirements);
|
|
}
|
|
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT_LOCAL(CreateInfo, *pCreateInfo).Important();
|
|
SERIALISE_ELEMENT_OPT(pAllocator);
|
|
SERIALISE_ELEMENT_LOCAL(Buffer, GetResID(*pBuffer)).TypedAs("VkBuffer"_lit);
|
|
// unused at the moment, just for user information
|
|
SERIALISE_ELEMENT(memoryRequirements);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
VkBuffer buf = VK_NULL_HANDLE;
|
|
|
|
VkBufferUsageFlags origusage = CreateInfo.usage;
|
|
|
|
// ensure we can always readback from buffers
|
|
CreateInfo.usage |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
|
|
|
// we only need to add TRANSFER_DST_BIT for dedicated buffers, but there's not a reliable way to
|
|
// know if a buffer will be dedicated-allocation or not. We assume that TRANSFER_DST is
|
|
// effectively free as a usage bit for all sensible implementations so we just add it here.
|
|
CreateInfo.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
|
|
|
// remap the queue family indices
|
|
if(CreateInfo.sharingMode == VK_SHARING_MODE_EXCLUSIVE)
|
|
{
|
|
uint32_t *queueFamiles = (uint32_t *)CreateInfo.pQueueFamilyIndices;
|
|
for(uint32_t q = 0; q < CreateInfo.queueFamilyIndexCount; q++)
|
|
queueFamiles[q] = m_QueueRemapping[queueFamiles[q]][0].family;
|
|
}
|
|
|
|
VkBufferCreateInfo patched = CreateInfo;
|
|
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(patched.pNext));
|
|
|
|
UnwrapNextChain(m_State, "VkBufferCreateInfo", tempMem, (VkBaseInStructure *)&patched);
|
|
|
|
VkResult ret = ObjDisp(device)->CreateBuffer(Unwrap(device), &patched, NULL, &buf);
|
|
|
|
if(CreateInfo.flags &
|
|
(VK_BUFFER_CREATE_SPARSE_BINDING_BIT | VK_BUFFER_CREATE_SPARSE_RESIDENCY_BIT))
|
|
{
|
|
APIProps.SparseResources = true;
|
|
}
|
|
|
|
CreateInfo.usage = origusage;
|
|
|
|
if(ret != VK_SUCCESS)
|
|
{
|
|
RDCERR("Failed on resource serialise-creation, VkResult: %s", ToStr(ret).c_str());
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ResourceId live = GetResourceManager()->WrapResource(Unwrap(device), buf);
|
|
GetResourceManager()->AddLiveResource(Buffer, buf);
|
|
|
|
m_CreationInfo.m_Buffer[live].Init(GetResourceManager(), m_CreationInfo, &CreateInfo);
|
|
}
|
|
|
|
AddResource(Buffer, ResourceType::Buffer, "Buffer");
|
|
DerivedResource(device, Buffer);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkCreateBuffer(VkDevice device, const VkBufferCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkBuffer *pBuffer)
|
|
{
|
|
VkBufferCreateInfo adjusted_info = *pCreateInfo;
|
|
|
|
// TEMP HACK: Until we define a portable fake hardware, need to match the requirements for usage
|
|
// on replay, so that the memory requirements are the same
|
|
adjusted_info.usage |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
|
|
|
// we only need to add TRANSFER_DST_BIT for dedicated buffers, but there's not a reliable way to
|
|
// know if a buffer will be dedicated-allocation or not. We assume that TRANSFER_DST is
|
|
// effectively free as a usage bit for all sensible implementations so we just add it here.
|
|
adjusted_info.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
|
|
|
// If we're using this buffer for device addresses, ensure we force on capture replay bit.
|
|
// We ensured the physical device can support this feature before whitelisting the extension.
|
|
if(IsCaptureMode(m_State) && (adjusted_info.usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT))
|
|
adjusted_info.flags |= VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT;
|
|
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(adjusted_info.pNext));
|
|
|
|
UnwrapNextChain(m_State, "VkBufferCreateInfo", tempMem, (VkBaseInStructure *)&adjusted_info);
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->CreateBuffer(Unwrap(device), &adjusted_info, pAllocator, pBuffer));
|
|
|
|
if(ret == VK_SUCCESS)
|
|
{
|
|
ResourceId id = GetResourceManager()->WrapResource(Unwrap(device), *pBuffer);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
VkBufferCreateInfo serialisedCreateInfo = *pCreateInfo;
|
|
VkBufferDeviceAddressCreateInfoEXT bufferDeviceAddressEXT = {
|
|
VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_CREATE_INFO_EXT,
|
|
};
|
|
VkBufferOpaqueCaptureAddressCreateInfo bufferDeviceAddressCoreOrKHR = {
|
|
VK_STRUCTURE_TYPE_BUFFER_OPAQUE_CAPTURE_ADDRESS_CREATE_INFO,
|
|
};
|
|
|
|
VkResourceRecord *record = GetResourceManager()->AddResourceRecord(*pBuffer);
|
|
record->memSize = pCreateInfo->size;
|
|
|
|
// if we're using VK_[KHR|EXT]_buffer_device_address, we fetch the device address that's been
|
|
// allocated and insert it into the next chain and patch the flags so that it replays
|
|
// naturally.
|
|
if((pCreateInfo->usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0)
|
|
{
|
|
VkBufferDeviceAddressInfo getInfo = {
|
|
VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, NULL, Unwrap(*pBuffer),
|
|
};
|
|
|
|
if(GetExtensions(GetRecord(device)).ext_KHR_buffer_device_address)
|
|
{
|
|
bufferDeviceAddressCoreOrKHR.opaqueCaptureAddress =
|
|
ObjDisp(device)->GetBufferOpaqueCaptureAddress(Unwrap(device), &getInfo);
|
|
|
|
// we explicitly DON'T assert on this, because some drivers will only need the device
|
|
// address specified at allocate time.
|
|
// RDCASSERT(bufferDeviceAddressKHR.opaqueCaptureAddress);
|
|
|
|
// push this struct onto the start of the chain
|
|
bufferDeviceAddressCoreOrKHR.pNext = serialisedCreateInfo.pNext;
|
|
serialisedCreateInfo.pNext = &bufferDeviceAddressCoreOrKHR;
|
|
}
|
|
else if(GetExtensions(GetRecord(device)).ext_EXT_buffer_device_address)
|
|
{
|
|
bufferDeviceAddressEXT.deviceAddress =
|
|
ObjDisp(device)->GetBufferDeviceAddressEXT(Unwrap(device), &getInfo);
|
|
|
|
RDCASSERT(bufferDeviceAddressEXT.deviceAddress);
|
|
|
|
// push this struct onto the start of the chain
|
|
bufferDeviceAddressEXT.pNext = serialisedCreateInfo.pNext;
|
|
serialisedCreateInfo.pNext = &bufferDeviceAddressEXT;
|
|
}
|
|
else
|
|
{
|
|
RDCERR("Device address bit specified but no device address extension enabled");
|
|
}
|
|
|
|
// tell the driver on replay that we're giving it a pre-allocated address to use
|
|
serialisedCreateInfo.flags |= VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT;
|
|
|
|
// this buffer must be forced to be in any captures, since we can't track when it's used by
|
|
// address
|
|
AddForcedReference(record);
|
|
|
|
{
|
|
SCOPED_READLOCK(m_CapTransitionLock);
|
|
m_DeviceAddressResources.IDs.push_back(record->GetResourceID());
|
|
}
|
|
}
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkCreateBuffer);
|
|
Serialise_vkCreateBuffer(ser, device, &serialisedCreateInfo, NULL, pBuffer);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
record->AddChunk(chunk);
|
|
|
|
record->storable = (pCreateInfo->usage & (VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
|
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT)) != 0;
|
|
|
|
bool isSparse = (pCreateInfo->flags & (VK_BUFFER_CREATE_SPARSE_BINDING_BIT |
|
|
VK_BUFFER_CREATE_SPARSE_RESIDENCY_BIT)) != 0;
|
|
|
|
bool isExternal = FindNextStruct(&adjusted_info,
|
|
VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO) != NULL;
|
|
|
|
if(isSparse)
|
|
{
|
|
// buffers are always bound opaquely and in arbitrary divisions, sparse residency
|
|
// only means not all the buffer needs to be bound, which is not that interesting for
|
|
// our purposes. We just need to make sure sparse buffers are dirty.
|
|
GetResourceManager()->MarkDirtyResource(id);
|
|
}
|
|
|
|
if(isSparse || isExternal)
|
|
{
|
|
record->resInfo = new ResourceInfo();
|
|
|
|
// pre-populate memory requirements
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), Unwrap(*pBuffer),
|
|
&record->resInfo->memreqs);
|
|
|
|
// initialise the sparse page table
|
|
if(isSparse)
|
|
record->resInfo->sparseTable.Initialise(pCreateInfo->size,
|
|
record->resInfo->memreqs.alignment & 0xFFFFFFFFU);
|
|
|
|
// for external buffers, try creating a non-external version and take the worst case of
|
|
// memory requirements, in case the non-external one (as we will replay it) needs more
|
|
// memory or a stricter alignment
|
|
if(isExternal)
|
|
{
|
|
bool removed =
|
|
RemoveNextStruct(&adjusted_info, VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO);
|
|
|
|
RDCASSERTMSG("Couldn't find next struct indicating external memory", removed);
|
|
|
|
VkBuffer tmpbuf = VK_NULL_HANDLE;
|
|
VkResult vkr = ObjDisp(device)->CreateBuffer(Unwrap(device), &adjusted_info, NULL, &tmpbuf);
|
|
|
|
if(vkr == VK_SUCCESS && tmpbuf != VK_NULL_HANDLE)
|
|
{
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), tmpbuf, &mrq);
|
|
|
|
if(mrq.size > 0)
|
|
{
|
|
RDCDEBUG("External buffer requires %llu bytes at %llu alignment, in %x memory types",
|
|
record->resInfo->memreqs.size, record->resInfo->memreqs.alignment,
|
|
record->resInfo->memreqs.memoryTypeBits);
|
|
RDCDEBUG(
|
|
"Non-external version requires %llu bytes at %llu alignment, in %x memory types",
|
|
mrq.size, mrq.alignment, mrq.memoryTypeBits);
|
|
|
|
record->resInfo->memreqs.size = RDCMAX(record->resInfo->memreqs.size, mrq.size);
|
|
record->resInfo->memreqs.alignment =
|
|
RDCMAX(record->resInfo->memreqs.size, mrq.alignment);
|
|
if((record->resInfo->memreqs.memoryTypeBits & mrq.memoryTypeBits) == 0)
|
|
{
|
|
RDCWARN(
|
|
"External buffer shares no memory types with non-external buffer. This buffer "
|
|
"will not be replayable.");
|
|
}
|
|
else
|
|
{
|
|
record->resInfo->memreqs.memoryTypeBits &= mrq.memoryTypeBits;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RDCERR("Failed to create temporary non-external buffer to find memory requirements: %s",
|
|
ToStr(vkr).c_str());
|
|
}
|
|
|
|
if(tmpbuf != VK_NULL_HANDLE)
|
|
ObjDisp(device)->DestroyBuffer(Unwrap(device), tmpbuf, NULL);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GetResourceManager()->AddLiveResource(id, *pBuffer);
|
|
|
|
m_CreationInfo.m_Buffer[id].Init(GetResourceManager(), m_CreationInfo, pCreateInfo);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CheckVkResult(ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkCreateBufferView(SerialiserType &ser, VkDevice device,
|
|
const VkBufferViewCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator,
|
|
VkBufferView *pView)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT_LOCAL(CreateInfo, *pCreateInfo).Important();
|
|
SERIALISE_ELEMENT_OPT(pAllocator);
|
|
SERIALISE_ELEMENT_LOCAL(View, GetResID(*pView)).TypedAs("VkBufferView"_lit);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
VkBufferView view = VK_NULL_HANDLE;
|
|
|
|
VkBufferViewCreateInfo unwrappedInfo = CreateInfo;
|
|
unwrappedInfo.buffer = Unwrap(unwrappedInfo.buffer);
|
|
VkResult ret = ObjDisp(device)->CreateBufferView(Unwrap(device), &unwrappedInfo, NULL, &view);
|
|
|
|
if(ret != VK_SUCCESS)
|
|
{
|
|
RDCERR("Failed on resource serialise-creation, VkResult: %s", ToStr(ret).c_str());
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ResourceId live;
|
|
|
|
if(GetResourceManager()->HasWrapper(ToTypedHandle(view)))
|
|
{
|
|
live = GetResourceManager()->GetNonDispWrapper(view)->id;
|
|
|
|
// destroy this instance of the duplicate, as we must have matching create/destroy
|
|
// calls and there won't be a wrapped resource hanging around to destroy this one.
|
|
ObjDisp(device)->DestroyBufferView(Unwrap(device), view, NULL);
|
|
|
|
// whenever the new ID is requested, return the old ID, via replacements.
|
|
GetResourceManager()->ReplaceResource(View, GetResourceManager()->GetOriginalID(live));
|
|
}
|
|
else
|
|
{
|
|
live = GetResourceManager()->WrapResource(Unwrap(device), view);
|
|
GetResourceManager()->AddLiveResource(View, view);
|
|
|
|
m_CreationInfo.m_BufferView[live].Init(GetResourceManager(), m_CreationInfo, &CreateInfo);
|
|
}
|
|
}
|
|
|
|
AddResource(View, ResourceType::View, "Buffer View");
|
|
DerivedResource(device, View);
|
|
DerivedResource(CreateInfo.buffer, View);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkCreateBufferView(VkDevice device, const VkBufferViewCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator,
|
|
VkBufferView *pView)
|
|
{
|
|
VkBufferViewCreateInfo unwrappedInfo = *pCreateInfo;
|
|
unwrappedInfo.buffer = Unwrap(unwrappedInfo.buffer);
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->CreateBufferView(Unwrap(device), &unwrappedInfo, pAllocator, pView));
|
|
|
|
if(ret == VK_SUCCESS)
|
|
{
|
|
ResourceId id = GetResourceManager()->WrapResource(Unwrap(device), *pView);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkCreateBufferView);
|
|
Serialise_vkCreateBufferView(ser, device, pCreateInfo, NULL, pView);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
VkResourceRecord *bufferRecord = GetRecord(pCreateInfo->buffer);
|
|
|
|
VkResourceRecord *record = GetResourceManager()->AddResourceRecord(*pView);
|
|
record->AddChunk(chunk);
|
|
record->AddParent(bufferRecord);
|
|
|
|
// store the base resource
|
|
record->baseResource = bufferRecord->GetResourceID();
|
|
record->baseResourceMem = bufferRecord->baseResource;
|
|
record->dedicated = bufferRecord->dedicated;
|
|
record->resInfo = bufferRecord->resInfo;
|
|
record->storable = bufferRecord->storable;
|
|
record->memOffset = bufferRecord->memOffset + pCreateInfo->offset;
|
|
record->memSize = pCreateInfo->range;
|
|
if(record->memSize == VK_WHOLE_SIZE)
|
|
record->memSize = bufferRecord->memSize - pCreateInfo->offset;
|
|
}
|
|
else
|
|
{
|
|
GetResourceManager()->AddLiveResource(id, *pView);
|
|
|
|
m_CreationInfo.m_BufferView[id].Init(GetResourceManager(), m_CreationInfo, pCreateInfo);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkCreateImage(SerialiserType &ser, VkDevice device,
|
|
const VkImageCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkImage *pImage)
|
|
{
|
|
VkMemoryRequirements memoryRequirements = {};
|
|
|
|
if(ser.IsWriting())
|
|
{
|
|
ObjDisp(device)->GetImageMemoryRequirements(Unwrap(device), Unwrap(*pImage), &memoryRequirements);
|
|
}
|
|
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT_LOCAL(CreateInfo, *pCreateInfo).Important();
|
|
SERIALISE_ELEMENT_OPT(pAllocator);
|
|
SERIALISE_ELEMENT_LOCAL(Image, GetResID(*pImage)).TypedAs("VkImage"_lit);
|
|
// unused at the moment, just for user information
|
|
SERIALISE_ELEMENT(memoryRequirements);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
VkImage img = VK_NULL_HANDLE;
|
|
|
|
VkImageUsageFlags origusage = CreateInfo.usage;
|
|
|
|
// ensure we can always display and copy from/to textures
|
|
CreateInfo.usage |= VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
CreateInfo.usage &= ~VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
|
|
|
|
// remap the queue family indices
|
|
if(CreateInfo.sharingMode == VK_SHARING_MODE_EXCLUSIVE)
|
|
{
|
|
uint32_t *queueFamiles = (uint32_t *)CreateInfo.pQueueFamilyIndices;
|
|
for(uint32_t q = 0; q < CreateInfo.queueFamilyIndexCount; q++)
|
|
queueFamiles[q] = m_QueueRemapping[queueFamiles[q]][0].family;
|
|
}
|
|
|
|
// need to be able to mutate the format for YUV textures
|
|
if(IsYUVFormat(CreateInfo.format))
|
|
CreateInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
|
|
|
// ensure we can cast multisampled images, for copying to arrays
|
|
if((int)CreateInfo.samples > 1)
|
|
{
|
|
CreateInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
|
|
|
// colour targets we do a simple compute copy, for depth-stencil we need
|
|
// to take a slower path that uses drawing
|
|
if(!IsDepthOrStencilFormat(CreateInfo.format))
|
|
{
|
|
// only add STORAGE_BIT if we have an Array2MS pipeline. If it failed to create due to lack
|
|
// of capability or because we disabled it as a workaround then we don't need this
|
|
// capability (and it might be the bug we're trying to work around by disabling the
|
|
// pipeline)
|
|
if(GetShaderCache()->IsArray2MSSupported())
|
|
CreateInfo.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
|
}
|
|
else
|
|
{
|
|
CreateInfo.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
}
|
|
}
|
|
|
|
APIProps.YUVTextures |= IsYUVFormat(CreateInfo.format);
|
|
|
|
const bool isSparse = (CreateInfo.flags & (VK_IMAGE_CREATE_SPARSE_BINDING_BIT |
|
|
VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT)) != 0;
|
|
|
|
if(isSparse)
|
|
{
|
|
APIProps.SparseResources = true;
|
|
}
|
|
|
|
// we search for the separate stencil usage struct now that it's in patchable memory
|
|
VkImageStencilUsageCreateInfo *separateStencilUsage =
|
|
(VkImageStencilUsageCreateInfo *)FindNextStruct(
|
|
&CreateInfo, VK_STRUCTURE_TYPE_IMAGE_STENCIL_USAGE_CREATE_INFO);
|
|
if(separateStencilUsage)
|
|
{
|
|
separateStencilUsage->stencilUsage |= VK_IMAGE_USAGE_SAMPLED_BIT |
|
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
separateStencilUsage->stencilUsage &= ~VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
|
|
|
|
if(CreateInfo.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
separateStencilUsage->stencilUsage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
}
|
|
}
|
|
|
|
rdcarray<VkFormat> patchedFormatList;
|
|
|
|
// similarly for the image format list for MSAA textures, add the UINT cast format we will need
|
|
if(CreateInfo.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
VkImageFormatListCreateInfo *formatListInfo = (VkImageFormatListCreateInfo *)FindNextStruct(
|
|
&CreateInfo, VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO);
|
|
|
|
if(formatListInfo)
|
|
{
|
|
uint32_t bs = GetByteSize(1, 1, 1, CreateInfo.format, 0);
|
|
|
|
VkFormat msaaCopyFormat = VK_FORMAT_UNDEFINED;
|
|
if(bs == 1)
|
|
msaaCopyFormat = VK_FORMAT_R8_UINT;
|
|
else if(bs == 2)
|
|
msaaCopyFormat = VK_FORMAT_R16_UINT;
|
|
else if(bs == 4)
|
|
msaaCopyFormat = VK_FORMAT_R32_UINT;
|
|
else if(bs == 8)
|
|
msaaCopyFormat = VK_FORMAT_R32G32_UINT;
|
|
else if(bs == 16)
|
|
msaaCopyFormat = VK_FORMAT_R32G32B32A32_UINT;
|
|
|
|
patchedFormatList.resize(formatListInfo->viewFormatCount + 1);
|
|
|
|
const VkFormat *oldFmts = formatListInfo->pViewFormats;
|
|
VkFormat *newFmts = patchedFormatList.data();
|
|
formatListInfo->pViewFormats = newFmts;
|
|
|
|
bool needAdded = true;
|
|
uint32_t i = 0;
|
|
for(; i < formatListInfo->viewFormatCount; i++)
|
|
{
|
|
newFmts[i] = oldFmts[i];
|
|
if(newFmts[i] == msaaCopyFormat)
|
|
needAdded = false;
|
|
}
|
|
|
|
if(needAdded)
|
|
{
|
|
newFmts[i] = msaaCopyFormat;
|
|
formatListInfo->viewFormatCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
VkImageCreateInfo patched = CreateInfo;
|
|
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(patched.pNext));
|
|
|
|
UnwrapNextChain(m_State, "VkImageCreateInfo", tempMem, (VkBaseInStructure *)&patched);
|
|
|
|
VkResult ret = ObjDisp(device)->CreateImage(Unwrap(device), &patched, NULL, &img);
|
|
|
|
CreateInfo.usage = origusage;
|
|
|
|
if(ret != VK_SUCCESS)
|
|
{
|
|
RDCERR("Failed on resource serialise-creation, VkResult: %s", ToStr(ret).c_str());
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ResourceId live = GetResourceManager()->WrapResource(Unwrap(device), img);
|
|
GetResourceManager()->AddLiveResource(Image, img);
|
|
|
|
m_CreationInfo.m_Image[live].Init(GetResourceManager(), m_CreationInfo, &CreateInfo);
|
|
|
|
bool inserted = false;
|
|
auto state = InsertImageState(img, live, CreateInfo, eFrameRef_Unknown, &inserted);
|
|
if(!inserted)
|
|
{
|
|
// Image state already existed.
|
|
state->wrappedHandle = img;
|
|
*state = state->InitialState();
|
|
}
|
|
|
|
if(isSparse)
|
|
state->isMemoryBound = true;
|
|
}
|
|
|
|
const char *prefix = "Image";
|
|
|
|
if(CreateInfo.imageType == VK_IMAGE_TYPE_1D)
|
|
{
|
|
prefix = CreateInfo.arrayLayers > 1 ? "1D Array Image" : "1D Image";
|
|
|
|
if(CreateInfo.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
|
|
prefix = "1D Color Attachment";
|
|
else if(CreateInfo.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
|
|
prefix = "1D Depth Attachment";
|
|
}
|
|
else if(CreateInfo.imageType == VK_IMAGE_TYPE_2D)
|
|
{
|
|
prefix = CreateInfo.arrayLayers > 1 ? "2D Array Image" : "2D Image";
|
|
|
|
if(CreateInfo.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
|
|
prefix = "2D Color Attachment";
|
|
else if(CreateInfo.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
|
|
prefix = "2D Depth Attachment";
|
|
else if(CreateInfo.usage & VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT)
|
|
prefix = "2D Fragment Density Map Attachment";
|
|
}
|
|
else if(CreateInfo.imageType == VK_IMAGE_TYPE_3D)
|
|
{
|
|
prefix = "3D Image";
|
|
|
|
if(CreateInfo.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
|
|
prefix = "3D Color Attachment";
|
|
else if(CreateInfo.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
|
|
prefix = "3D Depth Attachment";
|
|
}
|
|
|
|
AddResource(Image, ResourceType::Texture, prefix);
|
|
DerivedResource(device, Image);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkCreateImage(VkDevice device, const VkImageCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkImage *pImage)
|
|
{
|
|
VkImageCreateInfo createInfo_adjusted = *pCreateInfo;
|
|
|
|
createInfo_adjusted.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
|
|
// TEMP HACK: Until we define a portable fake hardware, need to match the requirements for usage
|
|
// on replay, so that the memory requirements are the same
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
createInfo_adjusted.usage |= VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
createInfo_adjusted.usage &= ~VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
|
|
}
|
|
|
|
// need to be able to mutate the format for YUV textures
|
|
if(IsYUVFormat(createInfo_adjusted.format))
|
|
createInfo_adjusted.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
|
|
|
if(createInfo_adjusted.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
createInfo_adjusted.usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
createInfo_adjusted.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
|
|
|
// TEMP HACK: matching replay requirements
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
if(!IsDepthOrStencilFormat(createInfo_adjusted.format))
|
|
{
|
|
// need to check the debug manager here since we might be creating this internal image from
|
|
// its constructor
|
|
if(GetDebugManager() && GetShaderCache()->IsArray2MSSupported())
|
|
createInfo_adjusted.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
|
}
|
|
else
|
|
{
|
|
createInfo_adjusted.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
}
|
|
}
|
|
}
|
|
|
|
// create non-subsampled image to be able to copy its content
|
|
createInfo_adjusted.flags &= ~VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT;
|
|
|
|
size_t tempMemSize = GetNextPatchSize(createInfo_adjusted.pNext);
|
|
|
|
// reserve space for a patched view format list if necessary
|
|
if(createInfo_adjusted.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
VkImageFormatListCreateInfo *formatListInfo = (VkImageFormatListCreateInfo *)FindNextStruct(
|
|
&createInfo_adjusted, VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO);
|
|
|
|
if(formatListInfo)
|
|
tempMemSize += sizeof(VkFormat) * (formatListInfo->viewFormatCount + 1);
|
|
}
|
|
|
|
byte *tempMem = GetTempMemory(tempMemSize);
|
|
|
|
UnwrapNextChain(m_State, "VkImageCreateInfo", tempMem, (VkBaseInStructure *)&createInfo_adjusted);
|
|
|
|
// we search for the separate stencil usage struct now that it's in patchable memory
|
|
VkImageStencilUsageCreateInfo *separateStencilUsage =
|
|
(VkImageStencilUsageCreateInfo *)FindNextStruct(
|
|
&createInfo_adjusted, VK_STRUCTURE_TYPE_IMAGE_STENCIL_USAGE_CREATE_INFO);
|
|
if(separateStencilUsage)
|
|
{
|
|
separateStencilUsage->stencilUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
createInfo_adjusted.usage |= VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
createInfo_adjusted.usage &= ~VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
|
|
}
|
|
|
|
if(createInfo_adjusted.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
separateStencilUsage->stencilUsage |=
|
|
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
}
|
|
}
|
|
|
|
// similarly for the image format list for MSAA textures, add the UINT cast format we will need
|
|
if(createInfo_adjusted.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
VkImageFormatListCreateInfo *formatListInfo = (VkImageFormatListCreateInfo *)FindNextStruct(
|
|
&createInfo_adjusted, VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO);
|
|
|
|
if(formatListInfo)
|
|
{
|
|
uint32_t bs = GetByteSize(1, 1, 1, createInfo_adjusted.format, 0);
|
|
|
|
VkFormat msaaCopyFormat = VK_FORMAT_UNDEFINED;
|
|
if(bs == 1)
|
|
msaaCopyFormat = VK_FORMAT_R8_UINT;
|
|
else if(bs == 2)
|
|
msaaCopyFormat = VK_FORMAT_R16_UINT;
|
|
else if(bs == 4)
|
|
msaaCopyFormat = VK_FORMAT_R32_UINT;
|
|
else if(bs == 8)
|
|
msaaCopyFormat = VK_FORMAT_R32G32_UINT;
|
|
else if(bs == 16)
|
|
msaaCopyFormat = VK_FORMAT_R32G32B32A32_UINT;
|
|
|
|
const VkFormat *oldFmts = formatListInfo->pViewFormats;
|
|
VkFormat *newFmts = (VkFormat *)tempMem;
|
|
formatListInfo->pViewFormats = newFmts;
|
|
|
|
bool needAdded = true;
|
|
uint32_t i = 0;
|
|
for(; i < formatListInfo->viewFormatCount; i++)
|
|
{
|
|
newFmts[i] = oldFmts[i];
|
|
if(newFmts[i] == msaaCopyFormat)
|
|
needAdded = false;
|
|
}
|
|
|
|
if(needAdded)
|
|
{
|
|
newFmts[i] = msaaCopyFormat;
|
|
formatListInfo->viewFormatCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->CreateImage(Unwrap(device), &createInfo_adjusted, pAllocator, pImage));
|
|
|
|
if(ret == VK_SUCCESS)
|
|
{
|
|
ResourceId id = GetResourceManager()->WrapResource(Unwrap(device), *pImage);
|
|
|
|
const bool isSparse = (pCreateInfo->flags & (VK_IMAGE_CREATE_SPARSE_BINDING_BIT |
|
|
VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT)) != 0;
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkCreateImage);
|
|
Serialise_vkCreateImage(ser, device, pCreateInfo, NULL, pImage);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
VkResourceRecord *record = GetResourceManager()->AddResourceRecord(*pImage);
|
|
record->AddChunk(chunk);
|
|
|
|
record->resInfo = new ResourceInfo();
|
|
ResourceInfo &resInfo = *record->resInfo;
|
|
resInfo.imageInfo = ImageInfo(*pCreateInfo);
|
|
|
|
record->storable = (pCreateInfo->usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0;
|
|
|
|
// pre-populate memory requirements
|
|
ObjDisp(device)->GetImageMemoryRequirements(Unwrap(device), Unwrap(*pImage), &resInfo.memreqs);
|
|
|
|
bool isLinear = (pCreateInfo->tiling == VK_IMAGE_TILING_LINEAR);
|
|
|
|
bool isExternal = false;
|
|
|
|
const VkBaseInStructure *next = (const VkBaseInStructure *)pCreateInfo->pNext;
|
|
|
|
// search for external memory image create info struct in pNext chain
|
|
while(next)
|
|
{
|
|
if(next->sType == VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO_NV ||
|
|
next->sType == VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO ||
|
|
next->sType == VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID)
|
|
{
|
|
isExternal = true;
|
|
break;
|
|
}
|
|
|
|
next = next->pNext;
|
|
}
|
|
|
|
// the image is immediately dirty because we don't use dirty tracking, it's too expensive to
|
|
// follow all frame refs in the background and it's pointless because memory almost always
|
|
// immediately becomes dirty anyway. The one case we might care about non-dirty memory is
|
|
// memory that has been allocated but not used, but that will be skipped or postponed as
|
|
// appropriate.
|
|
GetResourceManager()->MarkDirtyResource(id);
|
|
GetResourceManager()->MarkResourceFrameReferenced(id, eFrameRef_ReadBeforeWrite);
|
|
|
|
// sparse and external images should be considered dirty from creation anyway. For sparse
|
|
// images this is so that we can serialise the tracked page table, for external images this is
|
|
// so we can be sure to fetch their contents even if we don't see any writes.
|
|
//
|
|
// We also should consider linear images dirty since we may not get another chance - if they
|
|
// are bound to host-visible memory they may only be updated via memory maps, and we want to
|
|
// be sure to correctly copy their initial contents out rather than relying on memory contents
|
|
// (which may not be valid to map from/into if the image isn't in GENERAL layout).
|
|
if(isSparse || isExternal || isLinear)
|
|
{
|
|
GetResourceManager()->MarkDirtyResource(id);
|
|
GetResourceManager()->MarkResourceFrameReferenced(id, eFrameRef_ReadBeforeWrite);
|
|
|
|
// for external images, try creating a non-external version and take the worst case of
|
|
// memory requirements, in case the non-external one (as we will replay it) needs more
|
|
// memory or a stricter alignment
|
|
if(isExternal)
|
|
{
|
|
bool removed = false;
|
|
removed |= RemoveNextStruct(&createInfo_adjusted,
|
|
VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO_NV);
|
|
removed |= RemoveNextStruct(&createInfo_adjusted,
|
|
VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO);
|
|
removed |=
|
|
RemoveNextStruct(&createInfo_adjusted, VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID);
|
|
|
|
RDCASSERTMSG("Couldn't find next struct indicating external memory", removed);
|
|
|
|
VkImage tmpimg = VK_NULL_HANDLE;
|
|
VkResult vkr =
|
|
ObjDisp(device)->CreateImage(Unwrap(device), &createInfo_adjusted, NULL, &tmpimg);
|
|
|
|
if(vkr == VK_SUCCESS && tmpimg != VK_NULL_HANDLE)
|
|
{
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetImageMemoryRequirements(Unwrap(device), tmpimg, &mrq);
|
|
|
|
if(mrq.size > 0)
|
|
{
|
|
RDCDEBUG("External image requires %llu bytes at %llu alignment, in %x memory types",
|
|
resInfo.memreqs.size, resInfo.memreqs.alignment,
|
|
resInfo.memreqs.memoryTypeBits);
|
|
RDCDEBUG(
|
|
"Non-external version requires %llu bytes at %llu alignment, in %x memory types",
|
|
mrq.size, mrq.alignment, mrq.memoryTypeBits);
|
|
|
|
resInfo.memreqs.size = RDCMAX(resInfo.memreqs.size, mrq.size);
|
|
resInfo.memreqs.alignment = RDCMAX(resInfo.memreqs.size, mrq.alignment);
|
|
|
|
if((resInfo.memreqs.memoryTypeBits & mrq.memoryTypeBits) == 0)
|
|
{
|
|
RDCWARN(
|
|
"External image shares no memory types with non-external image. This image "
|
|
"will not be replayable.");
|
|
}
|
|
else
|
|
{
|
|
resInfo.memreqs.memoryTypeBits &= mrq.memoryTypeBits;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RDCERR("Failed to create temporary non-external image to find memory requirements: %s",
|
|
ToStr(vkr).c_str());
|
|
}
|
|
|
|
if(tmpimg != VK_NULL_HANDLE)
|
|
ObjDisp(device)->DestroyImage(Unwrap(device), tmpimg, NULL);
|
|
}
|
|
}
|
|
|
|
if(isSparse)
|
|
{
|
|
uint32_t pageByteSize = resInfo.memreqs.alignment & 0xFFFFFFFFu;
|
|
|
|
if(pCreateInfo->flags & VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT)
|
|
{
|
|
// must record image and page dimension, and create page tables
|
|
uint32_t numreqs = 8;
|
|
VkSparseImageMemoryRequirements reqs[8];
|
|
ObjDisp(device)->GetImageSparseMemoryRequirements(Unwrap(device), Unwrap(*pImage),
|
|
&numreqs, reqs);
|
|
|
|
// we only support at most DEPTH, STENCIL, METADATA = 3 aspects
|
|
RDCASSERT(numreqs > 0 && numreqs <= 3, numreqs);
|
|
|
|
// if we don't have just a single
|
|
resInfo.altSparseAspects.resize(numreqs - 1);
|
|
|
|
Sparse::Coord dim = {pCreateInfo->extent.width, pCreateInfo->extent.height,
|
|
pCreateInfo->extent.depth};
|
|
|
|
for(uint32_t r = 0; r < numreqs; r++)
|
|
{
|
|
if(r == 0)
|
|
resInfo.sparseAspect = reqs[r].formatProperties.aspectMask;
|
|
else
|
|
resInfo.altSparseAspects[r - 1].aspectMask = reqs[r].formatProperties.aspectMask;
|
|
|
|
Sparse::PageTable &table =
|
|
r == 0 ? resInfo.sparseTable : resInfo.altSparseAspects[r - 1].table;
|
|
|
|
bool singleMipTail =
|
|
(reqs[r].formatProperties.flags & VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT) != 0;
|
|
|
|
const VkExtent3D &gran = reqs[r].formatProperties.imageGranularity;
|
|
Sparse::Coord pageSize = {gran.width, gran.height, gran.depth};
|
|
|
|
table.Initialise(
|
|
dim, pCreateInfo->mipLevels, pCreateInfo->arrayLayers, pageByteSize, pageSize,
|
|
// we MIN here so if the driver returns 999 we have a consistent value, so we can
|
|
// compare against it on replay
|
|
RDCMIN(reqs[r].imageMipTailFirstLod, pCreateInfo->mipLevels),
|
|
reqs[r].imageMipTailOffset,
|
|
// if formatProperties.flags does not contain
|
|
// VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT (otherwise the value is undefined).
|
|
singleMipTail || pCreateInfo->arrayLayers == 0 ? 0 : reqs[r].imageMipTailStride,
|
|
// If formatProperties.flags contains VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT,
|
|
// this is the size of the whole mip tail, otherwise this is the size of the mip
|
|
// tail of a single array layer.
|
|
singleMipTail ? reqs[r].imageMipTailSize
|
|
: reqs[r].imageMipTailSize * pCreateInfo->arrayLayers);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// set page table up as if it were a buffer
|
|
resInfo.sparseTable.Initialise(resInfo.memreqs.size, pageByteSize);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GetResourceManager()->AddLiveResource(id, *pImage);
|
|
|
|
m_CreationInfo.m_Image[id].Init(GetResourceManager(), m_CreationInfo, pCreateInfo);
|
|
}
|
|
|
|
LockedImageStateRef state =
|
|
InsertImageState(*pImage, id, ImageInfo(*pCreateInfo), eFrameRef_None);
|
|
|
|
// sparse resources are always treated as if memory is bound, don't skip anything
|
|
if(isSparse)
|
|
state->isMemoryBound = true;
|
|
}
|
|
else
|
|
{
|
|
CheckVkResult(ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void WrappedVulkan::PatchImageViewUsage(VkImageViewUsageCreateInfo *usage, VkFormat imgFormat,
|
|
VkSampleCountFlagBits samples)
|
|
{
|
|
// this matches the mutations we do to images, so see vkCreateImage
|
|
usage->usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
usage->usage |= VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
usage->usage &= ~VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
|
|
|
|
if(samples != VK_SAMPLE_COUNT_1_BIT)
|
|
{
|
|
usage->usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
|
|
if(!IsDepthOrStencilFormat(imgFormat))
|
|
{
|
|
if(GetDebugManager() && GetShaderCache()->IsArray2MSSupported())
|
|
usage->usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
|
}
|
|
else
|
|
{
|
|
usage->usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Image view functions
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkCreateImageView(SerialiserType &ser, VkDevice device,
|
|
const VkImageViewCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator,
|
|
VkImageView *pView)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT_LOCAL(CreateInfo, *pCreateInfo).Important();
|
|
SERIALISE_ELEMENT_OPT(pAllocator);
|
|
SERIALISE_ELEMENT_LOCAL(View, GetResID(*pView)).TypedAs("VkImageView"_lit);
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
VkImageView view = VK_NULL_HANDLE;
|
|
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(&CreateInfo));
|
|
VkImageViewCreateInfo *unwrappedInfo = UnwrapStructAndChain(m_State, tempMem, &CreateInfo);
|
|
|
|
VkImageViewUsageCreateInfo *usageInfo = (VkImageViewUsageCreateInfo *)FindNextStruct(
|
|
unwrappedInfo, VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO);
|
|
|
|
if(usageInfo)
|
|
{
|
|
VkSampleCountFlagBits samples = m_CreationInfo.m_Image[GetResID(CreateInfo.image)].samples;
|
|
|
|
PatchImageViewUsage(usageInfo, CreateInfo.format, samples);
|
|
}
|
|
|
|
VkResult ret = ObjDisp(device)->CreateImageView(Unwrap(device), unwrappedInfo, NULL, &view);
|
|
|
|
APIProps.YUVTextures |= IsYUVFormat(CreateInfo.format);
|
|
|
|
if(ret != VK_SUCCESS)
|
|
{
|
|
RDCERR("Failed on resource serialise-creation, VkResult: %s", ToStr(ret).c_str());
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ResourceId live;
|
|
|
|
if(GetResourceManager()->HasWrapper(ToTypedHandle(view)))
|
|
{
|
|
live = GetResourceManager()->GetNonDispWrapper(view)->id;
|
|
|
|
// destroy this instance of the duplicate, as we must have matching create/destroy
|
|
// calls and there won't be a wrapped resource hanging around to destroy this one.
|
|
ObjDisp(device)->DestroyImageView(Unwrap(device), view, NULL);
|
|
|
|
// whenever the new ID is requested, return the old ID, via replacements.
|
|
GetResourceManager()->ReplaceResource(View, GetResourceManager()->GetOriginalID(live));
|
|
}
|
|
else
|
|
{
|
|
live = GetResourceManager()->WrapResource(Unwrap(device), view);
|
|
GetResourceManager()->AddLiveResource(View, view);
|
|
|
|
m_CreationInfo.m_ImageView[live].Init(GetResourceManager(), m_CreationInfo, &CreateInfo);
|
|
}
|
|
}
|
|
|
|
AddResource(View, ResourceType::View, "Image View");
|
|
DerivedResource(device, View);
|
|
DerivedResource(CreateInfo.image, View);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkCreateImageView(VkDevice device, const VkImageViewCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkImageView *pView)
|
|
{
|
|
byte *tempMem = GetTempMemory(GetNextPatchSize(pCreateInfo));
|
|
VkImageViewCreateInfo *unwrappedInfo = UnwrapStructAndChain(m_State, tempMem, pCreateInfo);
|
|
|
|
VkImageViewUsageCreateInfo *usageInfo = (VkImageViewUsageCreateInfo *)FindNextStruct(
|
|
unwrappedInfo, VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO);
|
|
|
|
if(usageInfo)
|
|
{
|
|
VkSampleCountFlagBits samples;
|
|
if(IsCaptureMode(m_State))
|
|
samples = (VkSampleCountFlagBits)GetRecord(pCreateInfo->image)->resInfo->imageInfo.sampleCount;
|
|
else
|
|
samples = m_CreationInfo.m_Image[GetResID(pCreateInfo->image)].samples;
|
|
|
|
PatchImageViewUsage(usageInfo, pCreateInfo->format, samples);
|
|
}
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->CreateImageView(Unwrap(device), unwrappedInfo, pAllocator, pView));
|
|
|
|
if(ret == VK_SUCCESS)
|
|
{
|
|
ResourceId id = GetResourceManager()->WrapResource(Unwrap(device), *pView);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
Chunk *chunk = NULL;
|
|
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkCreateImageView);
|
|
Serialise_vkCreateImageView(ser, device, pCreateInfo, NULL, pView);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
VkResourceRecord *imageRecord = GetRecord(pCreateInfo->image);
|
|
|
|
VkResourceRecord *record = GetResourceManager()->AddResourceRecord(*pView);
|
|
record->AddChunk(chunk);
|
|
record->AddParent(imageRecord);
|
|
|
|
// store the base resource. Note images have a baseResource pointing
|
|
// to their memory, which we will also need so we store that separately
|
|
record->baseResource = imageRecord->GetResourceID();
|
|
record->baseResourceMem = imageRecord->baseResource;
|
|
record->dedicated = imageRecord->dedicated;
|
|
record->resInfo = imageRecord->resInfo;
|
|
record->viewRange = pCreateInfo->subresourceRange;
|
|
record->viewRange.setViewType(pCreateInfo->viewType);
|
|
}
|
|
else
|
|
{
|
|
GetResourceManager()->AddLiveResource(id, *pView);
|
|
|
|
m_CreationInfo.m_ImageView[id].Init(GetResourceManager(), m_CreationInfo, pCreateInfo);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkBindBufferMemory2(SerialiserType &ser, VkDevice device,
|
|
uint32_t bindInfoCount,
|
|
const VkBindBufferMemoryInfo *pBindInfos)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT(bindInfoCount);
|
|
SERIALISE_ELEMENT_ARRAY(pBindInfos, bindInfoCount).Important();
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
for(uint32_t i = 0; i < bindInfoCount; i++)
|
|
{
|
|
const VkBindBufferMemoryInfo &bindInfo = pBindInfos[i];
|
|
|
|
ResourceId resOrigId = GetResourceManager()->GetOriginalID(GetResID(bindInfo.buffer));
|
|
ResourceId memOrigId = GetResourceManager()->GetOriginalID(GetResID(bindInfo.memory));
|
|
|
|
VulkanCreationInfo::Buffer &bufInfo = m_CreationInfo.m_Buffer[GetResID(bindInfo.buffer)];
|
|
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetBufferMemoryRequirements(Unwrap(device), Unwrap(bindInfo.buffer), &mrq);
|
|
|
|
bool ok =
|
|
CheckMemoryRequirements(("Buffer " + ToStr(resOrigId)).c_str(), GetResID(bindInfo.memory),
|
|
bindInfo.memoryOffset, mrq, bufInfo.external);
|
|
|
|
if(!ok)
|
|
return false;
|
|
|
|
GetResourceDesc(memOrigId).derivedResources.push_back(resOrigId);
|
|
GetResourceDesc(resOrigId).parentResources.push_back(memOrigId);
|
|
|
|
AddResourceCurChunk(memOrigId);
|
|
AddResourceCurChunk(resOrigId);
|
|
|
|
// for buffers created with device addresses, fetch it now as that's possible for both EXT and
|
|
// KHR variants now.
|
|
if(bufInfo.usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT)
|
|
{
|
|
VkBufferDeviceAddressInfo getInfo = {
|
|
VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, NULL, Unwrap(bindInfo.buffer),
|
|
};
|
|
|
|
if(GetExtensions(GetRecord(device)).ext_KHR_buffer_device_address)
|
|
bufInfo.gpuAddress = ObjDisp(device)->GetBufferDeviceAddress(Unwrap(device), &getInfo);
|
|
else if(GetExtensions(GetRecord(device)).ext_EXT_buffer_device_address)
|
|
bufInfo.gpuAddress = ObjDisp(device)->GetBufferDeviceAddressEXT(Unwrap(device), &getInfo);
|
|
}
|
|
|
|
// the memory is immediately dirty because we don't use dirty tracking, it's too expensive to
|
|
// follow all frame refs in the background and it's pointless because memory almost always
|
|
// immediately becomes dirty anyway. The one case we might care about non-dirty memory is
|
|
// memory that has been allocated but not used, but that will be skipped or postponed as
|
|
// appropriate.
|
|
GetResourceManager()->MarkDirtyResource(GetResID(bindInfo.memory));
|
|
|
|
m_CreationInfo.m_Memory[GetResID(bindInfo.memory)].BindMemory(
|
|
bindInfo.memoryOffset, mrq.size, VulkanCreationInfo::Memory::Linear);
|
|
}
|
|
|
|
VkBindBufferMemoryInfo *unwrapped = UnwrapInfos(pBindInfos, bindInfoCount);
|
|
ObjDisp(device)->BindBufferMemory2(Unwrap(device), bindInfoCount, unwrapped);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkBindBufferMemory2(VkDevice device, uint32_t bindInfoCount,
|
|
const VkBindBufferMemoryInfo *pBindInfos)
|
|
{
|
|
VkBindBufferMemoryInfo *unwrapped = UnwrapInfos(pBindInfos, bindInfoCount);
|
|
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->BindBufferMemory2(Unwrap(device), bindInfoCount, unwrapped));
|
|
|
|
CheckVkResult(ret);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
for(uint32_t i = 0; i < bindInfoCount; i++)
|
|
{
|
|
VkResourceRecord *bufrecord = GetRecord(pBindInfos[i].buffer);
|
|
VkResourceRecord *memrecord = GetRecord(pBindInfos[i].memory);
|
|
|
|
Chunk *chunk = NULL;
|
|
|
|
// we split this batch-bind up, so that each bind goes into the right record
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkBindBufferMemory2);
|
|
Serialise_vkBindBufferMemory2(ser, device, 1, pBindInfos + i);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
// memory object bindings are immutable and must happen before creation or use,
|
|
// so this can always go into the record, even if a resource is created and bound
|
|
// to memory mid-frame
|
|
bufrecord->AddChunk(chunk);
|
|
|
|
bufrecord->AddParent(memrecord);
|
|
bufrecord->baseResource = memrecord->GetResourceID();
|
|
bufrecord->dedicated = memrecord->memMapState->dedicated;
|
|
bufrecord->memOffset = pBindInfos[i].memoryOffset;
|
|
|
|
memrecord->storable |= bufrecord->storable;
|
|
|
|
// if the buffer was force-referenced, do the same with the memory
|
|
if(IsForcedReference(bufrecord))
|
|
{
|
|
// in case we're currently capturing, immediately consider the buffer and backing memory as
|
|
// read-before-write referenced
|
|
GetResourceManager()->MarkResourceFrameReferenced(bufrecord->GetResourceID(),
|
|
eFrameRef_ReadBeforeWrite);
|
|
GetResourceManager()->MarkMemoryFrameReferenced(
|
|
GetResID(pBindInfos[i].memory), pBindInfos[i].memoryOffset, bufrecord->memSize,
|
|
eFrameRef_ReadBeforeWrite);
|
|
}
|
|
|
|
// the memory is immediately dirty because we don't use dirty tracking, it's too expensive to
|
|
// follow all frame refs in the background and it's pointless because memory almost always
|
|
// immediately becomes dirty anyway. The one case we might care about non-dirty memory is
|
|
// memory that has been allocated but not used, but that will be skipped or postponed as
|
|
// appropriate.
|
|
GetResourceManager()->MarkDirtyResource(GetResID(pBindInfos[i].memory));
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
template <typename SerialiserType>
|
|
bool WrappedVulkan::Serialise_vkBindImageMemory2(SerialiserType &ser, VkDevice device,
|
|
uint32_t bindInfoCount,
|
|
const VkBindImageMemoryInfo *pBindInfos)
|
|
{
|
|
SERIALISE_ELEMENT(device);
|
|
SERIALISE_ELEMENT(bindInfoCount);
|
|
SERIALISE_ELEMENT_ARRAY(pBindInfos, bindInfoCount).Important();
|
|
|
|
SERIALISE_CHECK_READ_ERRORS();
|
|
|
|
if(IsReplayingAndReading())
|
|
{
|
|
for(uint32_t i = 0; i < bindInfoCount; i++)
|
|
{
|
|
const VkBindImageMemoryInfo &bindInfo = pBindInfos[i];
|
|
|
|
ResourceId resOrigId = GetResourceManager()->GetOriginalID(GetResID(bindInfo.image));
|
|
ResourceId memOrigId = GetResourceManager()->GetOriginalID(GetResID(bindInfo.memory));
|
|
|
|
VulkanCreationInfo::Image &imgInfo = m_CreationInfo.m_Image[GetResID(bindInfo.image)];
|
|
|
|
VkMemoryRequirements mrq = {};
|
|
ObjDisp(device)->GetImageMemoryRequirements(Unwrap(device), Unwrap(bindInfo.image), &mrq);
|
|
|
|
bool ok =
|
|
CheckMemoryRequirements(("Image " + ToStr(resOrigId)).c_str(), GetResID(bindInfo.memory),
|
|
bindInfo.memoryOffset, mrq, imgInfo.external);
|
|
|
|
if(!ok)
|
|
return false;
|
|
|
|
{
|
|
ResourceId id = GetResID(bindInfo.image);
|
|
LockedImageStateRef state = FindImageState(id);
|
|
if(!state)
|
|
{
|
|
RDCERR("Binding memory for unknown image %s", ToStr(id).c_str());
|
|
}
|
|
else
|
|
{
|
|
state->isMemoryBound = true;
|
|
state->boundMemory = GetResID(bindInfo.memory);
|
|
state->boundMemoryOffset = bindInfo.memoryOffset;
|
|
state->boundMemorySize = mrq.size;
|
|
}
|
|
}
|
|
|
|
GetResourceDesc(memOrigId).derivedResources.push_back(resOrigId);
|
|
GetResourceDesc(resOrigId).parentResources.push_back(memOrigId);
|
|
|
|
AddResourceCurChunk(memOrigId);
|
|
AddResourceCurChunk(resOrigId);
|
|
|
|
m_CreationInfo.m_Memory[GetResID(bindInfo.memory)].BindMemory(
|
|
bindInfo.memoryOffset, mrq.size,
|
|
imgInfo.linear ? VulkanCreationInfo::Memory::Linear : VulkanCreationInfo::Memory::Tiled);
|
|
}
|
|
|
|
VkBindImageMemoryInfo *unwrapped = UnwrapInfos(pBindInfos, bindInfoCount);
|
|
ObjDisp(device)->BindImageMemory2(Unwrap(device), bindInfoCount, unwrapped);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VkResult WrappedVulkan::vkBindImageMemory2(VkDevice device, uint32_t bindInfoCount,
|
|
const VkBindImageMemoryInfo *pBindInfos)
|
|
{
|
|
VkBindImageMemoryInfo *unwrapped = UnwrapInfos(pBindInfos, bindInfoCount);
|
|
VkResult ret;
|
|
SERIALISE_TIME_CALL(
|
|
ret = ObjDisp(device)->BindImageMemory2(Unwrap(device), bindInfoCount, unwrapped));
|
|
|
|
CheckVkResult(ret);
|
|
|
|
if(IsCaptureMode(m_State))
|
|
{
|
|
for(uint32_t i = 0; i < bindInfoCount; i++)
|
|
{
|
|
VkResourceRecord *imgrecord = GetRecord(pBindInfos[i].image);
|
|
VkResourceRecord *memrecord = GetRecord(pBindInfos[i].memory);
|
|
|
|
Chunk *chunk = NULL;
|
|
|
|
// we split this batch-bind up, so that each bind goes into the right record
|
|
{
|
|
CACHE_THREAD_SERIALISER();
|
|
|
|
SCOPED_SERIALISE_CHUNK(VulkanChunk::vkBindImageMemory2);
|
|
Serialise_vkBindImageMemory2(ser, device, 1, pBindInfos + i);
|
|
|
|
chunk = scope.Get();
|
|
}
|
|
|
|
{
|
|
ResourceId id = imgrecord->GetResourceID();
|
|
LockedImageStateRef state = FindImageState(id);
|
|
if(!state)
|
|
RDCERR("Binding memory for unknown image %s", ToStr(id).c_str());
|
|
else
|
|
state->isMemoryBound = true;
|
|
}
|
|
|
|
// memory object bindings are immutable and must happen before creation or use,
|
|
// so this can always go into the record, even if a resource is created and bound
|
|
// to memory mid-frame
|
|
imgrecord->AddChunk(chunk);
|
|
|
|
imgrecord->AddParent(memrecord);
|
|
|
|
// images are a base resource but we want to track where their memory comes from.
|
|
// Anything that looks up a baseResource for an image knows not to chase further
|
|
// than the image.
|
|
imgrecord->baseResource = memrecord->GetResourceID();
|
|
imgrecord->dedicated = memrecord->memMapState->dedicated;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(uint32_t i = 0; i < bindInfoCount; i++)
|
|
{
|
|
LockedImageStateRef state = FindImageState(GetResID(pBindInfos[i].image));
|
|
if(!state)
|
|
state->isMemoryBound = true;
|
|
else
|
|
RDCERR("Binding memory to unknown image %s", ToStr(GetResID(pBindInfos[i].image)).c_str());
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkAllocateMemory, VkDevice device,
|
|
const VkMemoryAllocateInfo *pAllocateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkDeviceMemory *pMemory);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(void, vkUnmapMemory, VkDevice device, VkDeviceMemory memory);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkFlushMappedMemoryRanges, VkDevice device,
|
|
uint32_t memoryRangeCount, const VkMappedMemoryRange *pMemoryRanges);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkBindBufferMemory, VkDevice device, VkBuffer buffer,
|
|
VkDeviceMemory memory, VkDeviceSize memoryOffset);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkBindImageMemory, VkDevice device, VkImage image,
|
|
VkDeviceMemory memory, VkDeviceSize memoryOffset);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkCreateBuffer, VkDevice device,
|
|
const VkBufferCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkBuffer *pBuffer);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkCreateBufferView, VkDevice device,
|
|
const VkBufferViewCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkBufferView *pView);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkCreateImage, VkDevice device,
|
|
const VkImageCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkImage *pImage);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkCreateImageView, VkDevice device,
|
|
const VkImageViewCreateInfo *pCreateInfo,
|
|
const VkAllocationCallbacks *pAllocator, VkImageView *pView);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkBindBufferMemory2, VkDevice device,
|
|
uint32_t bindInfoCount, const VkBindBufferMemoryInfo *pBindInfos);
|
|
|
|
INSTANTIATE_FUNCTION_SERIALISED(VkResult, vkBindImageMemory2, VkDevice device,
|
|
uint32_t bindInfoCount, const VkBindImageMemoryInfo *pBindInfos);
|