renderdoc/core/resource_manager.h (1,307 lines of code) (raw):
/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2024 Baldur Karlsson
* Copyright (c) 2014 Crytek
*
* 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.
******************************************************************************/
#pragma once
#include <algorithm>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include "api/replay/rdcflatmap.h"
#include "api/replay/resourceid.h"
#include "common/threading.h"
#include "core/core.h"
#include "os/os_specific.h"
#include "serialise/serialiser.h"
// In what way (read, write, etc) was a resource referenced in a frame -
// used to determine if initial contents are needed and to what degree.
// These values are used both as states (representing the cumulative previous
// accesses to the resource), and state transitions (access by a single
// command, modifying the state). This state machine is illustrated below,
// with states represented in caps, and transitions in lower case.
//
// +------------------ NONE -----------------------------+
// | | |
// read partialWrite completeWrite
// | | |
// V V V
// READ PARTIAL_WRITE --completeWrite--> COMPLETE_WRITE
// | |
// | read
// write |
// | V
// | WRITE_BEFORE_READ
// V |
// READ_BEFORE_WRITE <--write--+
//
// Note:
// * All resources begin implicitly in the None state.
// * The transitions labeled "write" correspond to either PartialWrite or
// CompleteWrite (e.g. in the READ state, either a PartialWrite or a
// CompleteWrite moves to the READ_BEFORE_WRITE state).
// * The state transitions for ReadBeforeWrite are simply the composition of
// the transition for read, followed by the transition for write (e.g.
// ReadBeforeWrite moves from NONE state to READBEFOREWRITE state);
// similarly, the state transitions for WriteBeforeRead are the composition
// of the transition for write, followed by the transition for read.
// * All other transitions (excluding ReadBeforeWrite and WriteBeforeRead)
// that are not explicitly shown leave the state unchanged (e.g. a read in
// the COMPLETE_WRITE state remains in the COMPLETE_WRITE state).
enum FrameRefType
{
// Initial state, no reads or writes
eFrameRef_None = 0,
// Write to some unknown subset of resource.
// As a state, this represents that unlike clear, some part of the
// initial contents might still be visible to later reads.
eFrameRef_PartialWrite = 1,
// Write to the entire resource.
// As a state, this represents that no later reads will even be able to see
// the initial contents, and therefore, the initial contents need not be
// restored for replay.
eFrameRef_CompleteWrite = 2,
// Read from the resource;
// As a state, this represents a read that could have seen the resource's
// initial contents, but the value seen by the read has not been overwritten;
// therefore, the initial contents needs to be restored before the first time
// we replay, but doesn't need to be reset between subsequent replays.
eFrameRef_Read = 3,
// Read followed by a write;
// As a state, this represents a read that could have seen the resource
// initial contents, followed by a write that could have modified that
// initial contents; therefore, the initial contents will need to be reset
// before each time we replay the frame.
eFrameRef_ReadBeforeWrite = 4,
// Partial write followed by read;
// For the purpose of correct replay, this is equivalent to `Read`. However,
// if this resource is inspected by the user before the write, the future
// read could, incorrectly, be observed. This is because read-only resources
// are not reset, so the write from the previous replay may still be present.
eFrameRef_WriteBeforeRead = 5,
// Similar to a CompleteWrite, but this is effectively an atomic "completely written, used, and
// discarded". This encodes more information than CompleteWrite because it tells us that the
// resource contents won't ever be used. For most purposes we treat this the same as
// CompleteWrite, but below we use it to identify skippable resources.
eFrameRef_CompleteWriteAndDiscard = 6,
// No reference info is available;
// This should only appear durring replay, and any (sub)resource with `Unknown`
// reference type should be conservatively reset before each replay.
eFrameRef_Unknown = 1000000000,
};
bool IncludesRead(FrameRefType refType);
bool IncludesWrite(FrameRefType refType);
const FrameRefType eFrameRef_Minimum = eFrameRef_None;
const FrameRefType eFrameRef_Maximum = eFrameRef_CompleteWriteAndDiscard;
// Threshold value for resource "age", i.e. how long it wasn't
// referred with the any write reference.
const double PERSISTENT_RESOURCE_AGE = 3000;
// how long since the first time it was skipped. If the skip state hasn't been invalidated in this
// long it can be skipped
const double SKIP_RESOURCE_AGE = 3000;
DECLARE_REFLECTION_ENUM(FrameRefType);
typedef FrameRefType (*FrameRefCompFunc)(FrameRefType, FrameRefType);
// Compose frame refs that occur in a known order.
// This can be thought of as a state (`first`) and a transition from that state
// (`second`), returning the new state (see the state diagram for
// `FrameRefType` above)
FrameRefType ComposeFrameRefs(FrameRefType first, FrameRefType second);
// Compose frame refs when the order is unknown.
// This is conservative, in that, if there is both a Read and a Write/Clear, it
// assumes the Read occurs before the Write/Clear, forcing that resource to be
// reset for replay.
FrameRefType ComposeFrameRefsUnordered(FrameRefType first, FrameRefType second);
// Compose frame refs for disjoint subresources.
// This is used to compute the overall frame ref for images/memory from the
// frame refs of their subresources.
FrameRefType ComposeFrameRefsDisjoint(FrameRefType x, FrameRefType y);
// Returns whichever of `first` or `second` is valid.
FrameRefType ComposeFrameRefsFirstKnown(FrameRefType first, FrameRefType second);
// Dummy frame ref composition that always keeps the old ref.
FrameRefType KeepOldFrameRef(FrameRefType first, FrameRefType second);
bool IsDirtyFrameRef(FrameRefType refType);
bool IsCompleteWriteFrameRef(FrameRefType refType);
// Captures the possible initialization/reset requirements for resources.
// These requirements are entirely determined by the resource's FrameRefType,
// but this type improves the readability of the code that checks
// init/reset requirements.
enum InitReqType
{
// No initialization required.
eInitReq_None,
// Initialize the resource by clearing.
eInitReq_Clear,
// Initialize the resource by copying initial data.
eInitReq_Copy,
};
enum InitPolicy
{
// Completely disable optimizations--copy initial data into every resource
// before every replay.
eInitPolicy_NoOpt,
// CopyAll--conservative policy which ensures each subresource begins each
// replay with the correct initial data.
//
// Initialization policy:
// Copy initial data into each subresource
//
// Reset policy:
// Copy initial data into each subresource which is written
eInitPolicy_CopyAll,
// ClearUnread--avoid copying initial data which is never read by the replay
// commands. A user inspecting a resource before it is written may observe
// cleared data, rather than the actual initial data.
//
// Initialization policy:
// Copy initial data into each subresource that is read.
// Clear each subresource that is not read.
//
// Reset policy:
// Copy initial data into each subresource where the initial data is read
// and then overwritten.
// Clear each subresource which is written, but whose initial data is not read.
eInitPolicy_ClearUnread,
// Fastest--Initialize/reset as little as possible for correct replay.
// A user inspecting a resource before it is written may observe the data
// from a future write (from the previous replay).
//
// Initialization policy:
// Copy initial data into each subresource that is read.
// Clear each subresource that is not read.
//
// Reset policy:
// Copy initial data into each subresource where the initial data is read
// and then overwritten.
eInitPolicy_Fastest,
};
// Return the initialization/reset requirements for a FrameRefType
inline InitReqType InitReq(FrameRefType refType, InitPolicy policy, bool initialized)
{
if(eFrameRef_Minimum > refType || refType > eFrameRef_Maximum)
return eInitReq_Copy;
#define COPY_ONCE (initialized ? eInitReq_None : eInitReq_Copy)
#define CLEAR_ONCE (initialized ? eInitReq_None : eInitReq_Clear)
switch(policy)
{
case eInitPolicy_NoOpt: return eInitReq_Copy;
case eInitPolicy_CopyAll:
switch(refType)
{
case eFrameRef_None: return COPY_ONCE;
case eFrameRef_Read: return COPY_ONCE;
default: return eInitReq_Copy;
}
case eInitPolicy_ClearUnread:
switch(refType)
{
case eFrameRef_None: return CLEAR_ONCE;
case eFrameRef_Read: return COPY_ONCE;
case eFrameRef_ReadBeforeWrite: return eInitReq_Copy;
case eFrameRef_WriteBeforeRead: return eInitReq_Copy;
default: return eInitReq_Clear;
}
case eInitPolicy_Fastest:
switch(refType)
{
case eFrameRef_None: return CLEAR_ONCE;
case eFrameRef_Read: return COPY_ONCE;
case eFrameRef_ReadBeforeWrite: return eInitReq_Copy;
case eFrameRef_WriteBeforeRead: return COPY_ONCE;
default: return CLEAR_ONCE;
}
default: RDCERR("Unknown initialization policy (%d).", policy); return eInitReq_Copy;
}
#undef COPY_ONCE
#undef CLEAR_ONCE
}
// handle marking a resource referenced for read or write and storing RAW access etc.
template <typename Compose>
bool MarkReferenced(std::unordered_map<ResourceId, FrameRefType> &refs, ResourceId id,
FrameRefType refType, Compose comp)
{
auto refit = refs.find(id);
if(refit == refs.end())
{
refs[id] = refType;
return true;
}
else
{
refit->second = comp(refit->second, refType);
}
return false;
}
inline bool MarkReferenced(std::unordered_map<ResourceId, FrameRefType> &refs, ResourceId id,
FrameRefType refType)
{
return MarkReferenced(refs, id, refType, ComposeFrameRefs);
}
// verbose prints with IDs of each dirty resource and whether it was prepared,
// and whether it was serialised.
#define VERBOSE_DIRTY_RESOURCES OPTION_OFF
namespace ResourceIDGen
{
ResourceId GetNewUniqueID();
void SetReplayResourceIDs();
};
struct ResourceRecord;
class ResourceRecordHandler
{
public:
virtual void MarkDirtyResource(ResourceId id) = 0;
virtual void RemoveResourceRecord(ResourceId id) = 0;
virtual void MarkResourceFrameReferenced(ResourceId id, FrameRefType refType) = 0;
virtual void DestroyResourceRecord(ResourceRecord *record) = 0;
};
// This is a generic resource record, that APIs can inherit from and use.
// A resource is an API object that gets tracked on its own, has dependencies on other resources
// and has its own stream of chunks.
//
// This is used to track the necessary resources for a frame, and include only those required
// for the captured frame in its log. It also handles anything resource-specific such as
// shadow CPU copies of data.
struct ResourceRecord
{
ResourceRecord(ResourceId id, bool lock)
: RefCount(1),
ResID(id),
UpdateCount(0),
DataInSerialiser(false),
DataPtr(NULL),
DataOffset(0),
Length(0),
DataWritten(false),
InternalResource(false)
{
m_ChunkLock = NULL;
if(lock)
m_ChunkLock = new Threading::CriticalSection();
}
void DisableChunkLocking() { SAFE_DELETE(m_ChunkLock); }
~ResourceRecord() { SAFE_DELETE(m_ChunkLock); }
void AddParent(ResourceRecord *r)
{
if(r == this)
return;
if(Parents.indexOf(r) < 0)
{
r->AddRef();
Parents.push_back(r);
}
}
bool HasParent(ResourceRecord *r) const { return Parents.indexOf(r) >= 0; }
void MarkParentsDirty(ResourceRecordHandler *mgr)
{
for(auto it = Parents.begin(); it != Parents.end(); ++it)
mgr->MarkDirtyResource((*it)->GetResourceID());
}
void MarkParentsReferenced(ResourceRecordHandler *mgr, FrameRefType refType)
{
for(auto it = Parents.begin(); it != Parents.end(); ++it)
mgr->MarkResourceFrameReferenced((*it)->GetResourceID(), refType);
}
void FreeParents(ResourceRecordHandler *mgr)
{
for(auto it = Parents.begin(); it != Parents.end(); ++it)
(*it)->Delete(mgr);
Parents.clear();
}
void MarkDataUnwritten() { DataWritten = false; }
void Insert(std::map<int64_t, Chunk *> &recordlist)
{
bool dataWritten = DataWritten;
DataWritten = true;
for(auto it = Parents.begin(); it != Parents.end(); ++it)
{
if(!(*it)->DataWritten)
{
(*it)->Insert(recordlist);
}
}
if(!dataWritten)
{
for(auto it = m_Chunks.begin(); it != m_Chunks.end(); ++it)
recordlist[it->id] = it->chunk;
}
}
void AddRef() { Atomic::Inc32(&RefCount); }
int GetRefCount() const { return RefCount; }
void Delete(ResourceRecordHandler *mgr);
ResourceId GetResourceID() const { return ResID; }
void AddChunk(Chunk *chunk, int64_t ID = 0)
{
if(ID == 0)
ID = GetID();
LockChunks();
m_Chunks.push_back(StoredChunk(ID, chunk));
UnlockChunks();
}
void LockChunks()
{
if(m_ChunkLock)
m_ChunkLock->Lock();
}
void UnlockChunks()
{
if(m_ChunkLock)
m_ChunkLock->Unlock();
}
bool HasChunks() const { return !m_Chunks.empty(); }
size_t NumChunks() const { return m_Chunks.size(); }
void SwapChunks(ResourceRecord *other)
{
LockChunks();
other->LockChunks();
m_Chunks.swap(other->m_Chunks);
m_FrameRefs.swap(other->m_FrameRefs);
other->UnlockChunks();
UnlockChunks();
}
void AppendFrom(ResourceRecord *other)
{
LockChunks();
other->LockChunks();
for(auto it = other->m_Chunks.begin(); it != other->m_Chunks.end(); ++it)
AddChunk(it->chunk->Duplicate());
for(auto it = other->Parents.begin(); it != other->Parents.end(); ++it)
AddParent(*it);
other->UnlockChunks();
UnlockChunks();
}
void DeleteChunks()
{
LockChunks();
for(auto it = m_Chunks.begin(); it != m_Chunks.end(); ++it)
it->chunk->Delete(it->fromAllocator != 0);
m_Chunks.clear();
UnlockChunks();
}
Chunk *GetLastChunk() const
{
RDCASSERT(HasChunks());
return m_Chunks.back().chunk;
}
int64_t GetLastChunkID() const
{
RDCASSERT(HasChunks());
return m_Chunks.back().id;
}
void PopChunk() { m_Chunks.pop_back(); }
byte *GetDataPtr() { return DataPtr + DataOffset; }
bool HasDataPtr() { return DataPtr != NULL; }
void SetDataOffset(uint64_t offs) { DataOffset = offs; }
void SetDataPtr(byte *ptr) { DataPtr = ptr; }
template <typename Compose>
bool MarkResourceFrameReferenced(ResourceId id, FrameRefType refType, Compose comp);
inline bool MarkResourceFrameReferenced(ResourceId id, FrameRefType refType)
{
return MarkResourceFrameReferenced(id, refType, ComposeFrameRefs);
}
void AddResourceReferences(ResourceRecordHandler *mgr);
void AddReferencedIDs(std::unordered_set<ResourceId> &ids)
{
for(auto it = m_FrameRefs.begin(); it != m_FrameRefs.end(); ++it)
ids.insert(it->first);
}
uint64_t Length;
int UpdateCount;
bool DataInSerialiser;
// anything internal that shouldn't be automatically pulled in by 'Ref All Resources' or have
// initial contents stored. This could either be a type of object that would break if its chunks
// were inserted into the initialisation phase (like an D3D11 DeviceContext which contains
// commands) or just debug objects created during capture as helpers which shouldn't be included.
//
// The implication is that they are handled specially for being inserted into the capture, or
// aren't inserted at all. Note that if a resource is frame-referenced, it will be included
// regardless but still without initial contents, capture drivers should be careful.
bool InternalResource;
bool DataWritten;
protected:
int32_t RefCount;
byte *DataPtr;
uint64_t DataOffset;
ResourceId ResID;
rdcarray<ResourceRecord *> Parents;
int64_t GetID()
{
static int64_t globalIDCounter = 10;
return Atomic::Inc64(&globalIDCounter);
}
struct StoredChunk
{
StoredChunk(int64_t i, Chunk *c)
{
id = i;
// we store this here because by the time it comes to delete the chunks the allocator may have
// already been reset and the contents trashed.
fromAllocator = c->IsFromAllocator() ? 1 : 0;
chunk = c;
}
int64_t id : 63;
int64_t fromAllocator : 1;
Chunk *chunk;
};
rdcarray<StoredChunk> m_Chunks;
Threading::CriticalSection *m_ChunkLock;
std::unordered_map<ResourceId, FrameRefType> m_FrameRefs;
};
template <typename Compose>
bool ResourceRecord::MarkResourceFrameReferenced(ResourceId id, FrameRefType refType, Compose comp)
{
if(id == ResourceId())
return false;
return MarkReferenced(m_FrameRefs, id, refType, comp);
}
// the resource manager is a utility class that's not required but is likely wanted by any API
// implementation.
// It keeps track of resource records, which resources are alive and allows you to query for them by
// ID. It tracks
// which resources are marked as dirty (needing their initial contents fetched before capture).
//
// For APIs that wrap their resources it provides tracking for that.
//
// In the replay application it will also track which 'live' resources are representing which
// 'original'
// resources from the application when it was captured.
template <typename Configuration>
class ResourceManager : public ResourceRecordHandler
{
public:
typedef typename Configuration::WrappedResourceType WrappedResourceType;
typedef typename Configuration::RealResourceType RealResourceType;
typedef typename Configuration::RecordType RecordType;
typedef typename Configuration::InitialContentData InitialContentData;
ResourceManager(CaptureState &state);
virtual ~ResourceManager();
void Shutdown();
///////////////////////////////////////////
// Capture-side methods
// while capturing, resource records containing chunk streams and metadata for resources
RecordType *GetResourceRecord(ResourceId id);
bool HasResourceRecord(ResourceId id);
RecordType *AddResourceRecord(ResourceId id);
inline void RemoveResourceRecord(ResourceId id);
void DestroyResourceRecord(ResourceRecord *record);
// while capturing or replaying, resources and their live IDs
void AddCurrentResource(ResourceId id, WrappedResourceType res);
bool HasCurrentResource(ResourceId id);
WrappedResourceType GetCurrentResource(ResourceId id);
void ReleaseCurrentResource(ResourceId id);
// insert the chunks for the resources referenced in the frame
void InsertReferencedChunks(WriteSerialiser &ser);
// mark resource records as unwritten, ready to be written to a new logfile.
void MarkUnwrittenResources();
// clear the list of frame-referenced resources - e.g. if you're about to recapture a frame
void ClearReferencedResources();
// indicates this resource could have been modified by the GPU,
// so it's now suspect and the data we have on it might well be out of date
// and to be correct its contents should be serialised out at the start
// of the frame.
inline void MarkDirtyResource(ResourceId res);
// returns if the resource has been marked as dirty
bool IsResourceDirty(ResourceId res);
// call callbacks to prepare initial contents for dirty resources
void PrepareInitialContents();
InitialContentData GetInitialContents(ResourceId id);
void SetInitialContents(ResourceId id, InitialContentData contents);
void SetInitialChunk(ResourceId id, Chunk *chunk);
void SetInitialFileStore(ResourceId id, const rdcstr &filename, uint64_t start, uint64_t end);
// generate chunks for initial contents and insert.
void InsertInitialContentsChunks(WriteSerialiser &ser);
// for initial contents that don't need a chunk - apply them here. This allows any patching to
// creation-time chunks to happen before they're written to disk.
void ApplyInitialContentsNonChunks(WriteSerialiser &ser);
// Serialise out which resources need initial contents, along with whether their
// initial contents are in the serialised stream (e.g. RTs might still want to be
// cleared on frame init).
void Serialise_InitialContentsNeeded(WriteSerialiser &ser);
// mark resource referenced somewhere in the main frame-affecting calls.
// That means this resource should be included in the final serialise out
template <typename Compose>
void MarkResourceFrameReferenced(ResourceId id, FrameRefType refType, Compose comp);
inline void MarkResourceFrameReferenced(ResourceId id, FrameRefType refType);
void MarkBackgroundFrameReferenced(const rdcflatmap<ResourceId, FrameRefType> &refs);
void CleanBackgroundFrameReferences();
///////////////////////////////////////////
// Replay-side methods
// Live resources to replace serialised IDs
void AddLiveResource(ResourceId origid, WrappedResourceType livePtr);
bool HasLiveResource(ResourceId origid);
WrappedResourceType GetLiveResource(ResourceId origid, bool optional = false);
void EraseLiveResource(ResourceId origid);
// when asked for a given id, return the resource for a replacement id
void ReplaceResource(ResourceId from, ResourceId to);
bool HasReplacement(ResourceId from);
void RemoveReplacement(ResourceId id);
// get the original ID for a real ID that may be a replacement. i.e. if ID 123 is ID 10000005
// live, and 10000005 live is replaced with 10000839, then calling this function with either ID
// 10000005 or ID 10000839 will return ID 123.
ResourceId GetUnreplacedOriginalID(ResourceId id);
// fetch original ID for a real ID or vice-versa.
ResourceId GetOriginalID(ResourceId id);
ResourceId GetLiveID(ResourceId id);
// Serialise in which resources need initial contents and set them up.
void CreateInitialContents(ReadSerialiser &ser);
// Free any initial contents that are prepared (for after capture is complete)
void FreeInitialContents();
// Apply the initial contents for the resources that need them, used at the start of a frame
void ApplyInitialContents();
// Resource wrapping, allows for querying and adding/removing of wrapper layers around resources
bool AddWrapper(WrappedResourceType wrap, RealResourceType real);
bool HasWrapper(RealResourceType real);
WrappedResourceType GetWrapper(RealResourceType real);
void RemoveWrapper(RealResourceType real);
void ResetLastWriteTimes();
void ResetCaptureStartTime();
bool HasPersistentAge(ResourceId id);
bool HasSkippableAge(ResourceId id);
bool IsResourcePostponed(ResourceId id);
bool IsResourceSkipped(ResourceId id);
bool ShouldPostpone(ResourceId id);
bool ShouldSkip(ResourceId id);
virtual bool IsResourceTrackedForPersistency(const WrappedResourceType &res) { return false; }
protected:
friend InitialContentData;
// 'interface' to implement by derived classes
virtual ResourceId GetID(WrappedResourceType res) = 0;
virtual bool ResourceTypeRelease(WrappedResourceType res) = 0;
virtual bool Need_InitialStateChunk(ResourceId id, const InitialContentData &initial)
{
return true;
}
virtual void Begin_PrepareInitialBatch() {}
virtual bool Prepare_InitialState(WrappedResourceType res) = 0;
virtual void End_PrepareInitialBatch() {}
virtual uint64_t GetSize_InitialState(ResourceId id, const InitialContentData &initial) = 0;
virtual bool Serialise_InitialState(WriteSerialiser &ser, ResourceId id, RecordType *record,
const InitialContentData *initialData) = 0;
virtual void Create_InitialState(ResourceId id, WrappedResourceType live, bool hasData) = 0;
virtual void Apply_InitialState(WrappedResourceType live, const InitialContentData &initial) = 0;
virtual rdcarray<ResourceId> InitialContentResources();
void UpdateLastWriteTime(ResourceId id, FrameRefType refType);
void Prepare_InitialStateIfPostponed(ResourceId id, bool midframe);
void SkipOrPostponeOrPrepare_InitialState(ResourceId id, FrameRefType refType);
// very coarse lock, protects EVERYTHING. This could certainly be improved and it may be a
// bottleneck for performance. Given that the main use cases are write-rarely read-often the lock
// should be optimised for that as we only want to make sure we're not modifying the objects
// together, by far the most common operation is looking up data.
Threading::CriticalSection m_Lock;
// we only need to lock during capturing, on replay we have single threaded access.
bool m_Capturing;
// easy optimisation win - don't use maps everywhere. It's convenient but not optimal, and
// profiling will likely prove that some or all of these could be a problem
// used during capture - map from real resource to its wrapper (other way can be done just with an
// Unwrap)
std::map<RealResourceType, WrappedResourceType> m_WrapperMap;
// used during capture - holds resources referenced in current frame (and how they're referenced)
std::unordered_map<ResourceId, FrameRefType> m_FrameReferencedResources;
// used during capture - holds resources marked as dirty, needing initial contents
std::set<ResourceId> m_DirtyResources;
struct InitialContentStorage
{
Chunk *chunk = NULL;
InitialContentData data;
rdcstr filename;
uint64_t fileStart = 0, fileEnd = 0;
void Free(ResourceManager *mgr)
{
if(chunk)
{
chunk->Delete();
chunk = NULL;
}
data.Free(mgr);
}
};
// used during capture or replay - holds initial contents
std::unordered_map<ResourceId, InitialContentStorage> m_InitialContents;
// used during capture or replay - map of resources currently alive with their real IDs, used in
// capture and replay.
std::unordered_map<ResourceId, WrappedResourceType> m_CurrentResourceMap;
// used during replay - maps back and forth from original id to live id and vice-versa
std::unordered_map<ResourceId, ResourceId> m_OriginalIDs, m_LiveIDs;
// used during replay - holds resources allocated and the original id that they represent
std::unordered_map<ResourceId, WrappedResourceType> m_LiveResourceMap;
// used during capture - holds resource records by id.
std::unordered_map<ResourceId, RecordType *> m_ResourceRecords;
Threading::RWLock m_ResourceRecordLock;
// used during replay - holds current resource replacements
// replaced -> replacement
std::unordered_map<ResourceId, ResourceId> m_Replacements;
// replacement -> replaced (for looking up original IDs)
std::unordered_map<ResourceId, ResourceId> m_Replaced;
// During initial resources preparation, persistent resources are
// postponed until serializing to RDC file.
std::unordered_set<ResourceId> m_PostponedResourceIDs;
// During initial resources preparation, resources that are completely written
// over are skipped
std::unordered_set<ResourceId> m_SkippedResourceIDs;
struct ResourceRefTimes
{
ResourceId id;
// On marking resource write-referenced in frame, its last write time is reset. The time is used
// to determine persistent resources, and is checked against the `PERSISTENT_RESOURCE_AGE`.
double writeTime;
// firstSkipTime is referring to the first time we saw a resource get marked as skippable.
// Any write will reset this time to 0.0 (not skippable), so if the firstSkipTime is longer than
// `SKIP_RESOURCE_AGE` ago then we know the resource has only ever been skipped (or not written)
// for that long. If the time is 0.0 then it's been written in a more visible way recently or
// has never been marked skippable.
double firstSkipTime;
bool operator<(const ResourceId &o) const { return id < o; }
};
// all resources that are written in some way end up in this list. We then check the last time
// they were written, and the last time they were ever partially used (not completely overwritten
// in one atomic chunk).
rdcarray<ResourceRefTimes> m_ResourceRefTimes;
// Timestamp at the beginning of the frame capture. Used to determine which
// resources to refresh for their last write or partial use time (see `ResourceRefTimes`).
double m_captureStartTime;
PerformanceTimer m_ResourcesUpdateTimer;
// The capture state is propagated by a specific driver.
CaptureState &m_State;
};
template <typename Configuration>
ResourceManager<Configuration>::ResourceManager(CaptureState &state) : m_State(state)
{
m_Capturing = IsCaptureMode(state);
RenderDoc::Inst().RegisterMemoryRegion(this, sizeof(ResourceManager));
}
template <typename Configuration>
void ResourceManager<Configuration>::Shutdown()
{
FreeInitialContents();
while(!m_LiveResourceMap.empty())
{
auto it = m_LiveResourceMap.begin();
ResourceId id = it->first;
ResourceTypeRelease(it->second);
auto removeit = m_LiveResourceMap.find(id);
if(removeit != m_LiveResourceMap.end())
m_LiveResourceMap.erase(removeit);
}
RDCASSERT(m_ResourceRecords.empty());
}
template <typename Configuration>
ResourceManager<Configuration>::~ResourceManager()
{
RDCASSERT(m_LiveResourceMap.empty());
RDCASSERT(m_InitialContents.empty());
RDCASSERT(m_ResourceRecords.empty());
RenderDoc::Inst().UnregisterMemoryRegion(this);
}
template <typename Configuration>
void ResourceManager<Configuration>::MarkBackgroundFrameReferenced(
const rdcflatmap<ResourceId, FrameRefType> &refs)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(IsBackgroundCapturing(m_State))
{
if(refs.size() <= m_ResourceRefTimes.size())
{
for(auto it = refs.begin(); it != refs.end(); ++it)
UpdateLastWriteTime(it->first, it->second);
}
else
{
for(const ResourceRefTimes &res : m_ResourceRefTimes)
{
auto it = refs.find(res.id);
if(it != refs.end())
UpdateLastWriteTime(it->first, it->second);
}
}
}
}
template <typename Configuration>
void ResourceManager<Configuration>::CleanBackgroundFrameReferences()
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(IsBackgroundCapturing(m_State))
{
double now = m_ResourcesUpdateTimer.GetMilliseconds();
// retire any old entries, if they were written once they shouldn't be tracked forever. This
// means the list only keeps track of recently written resources, not all resources that have
// ever been written.
//
// walk the array. If we find an item we want to remove we increment src otherwise we copy src
// to dst (if they are different). Thus next time dst will point at the item we want to skip, at
// each stage copying src to dst (if they are different). If we find an item we want to remove
// we increment src and continue (thus it will get copied ove
size_t dst = 0, src = 0;
for(dst = 0, src = 0; src < m_ResourceRefTimes.size();)
{
ResourceRefTimes &check = m_ResourceRefTimes[src];
// if this isn't skippable, and the write time was a long time ago then we can delete it.
// Resources not in the list are treated as if they were written an infinite time ago and so
// are postponable.
if(now - check.writeTime > PERSISTENT_RESOURCE_AGE && check.firstSkipTime == 0.0)
{
// skip src, check the next one
src++;
continue;
}
// we want to keep src. If dst == src we can just continue on to the next iteration, if not
// then we need to copy src into dst (where dst is the leftovers from previously remove
// entries)
if(dst != src)
m_ResourceRefTimes[dst] = m_ResourceRefTimes[src];
dst++;
src++;
}
m_ResourceRefTimes.resize(dst);
}
}
template <typename Configuration>
template <typename Compose>
void ResourceManager<Configuration>::MarkResourceFrameReferenced(ResourceId id,
FrameRefType refType, Compose comp)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(id == ResourceId())
return;
if(IsActiveCapturing(m_State))
{
SkipOrPostponeOrPrepare_InitialState(id, refType);
if(IsDirtyFrameRef(refType))
Prepare_InitialStateIfPostponed(id, true);
}
UpdateLastWriteTime(id, refType);
if(IsBackgroundCapturing(m_State))
return;
bool newRef = MarkReferenced(m_FrameReferencedResources, id, refType, comp);
if(newRef)
{
RecordType *record = GetResourceRecord(id);
if(record)
record->AddRef();
}
}
template <typename Configuration>
void ResourceManager<Configuration>::MarkResourceFrameReferenced(ResourceId id, FrameRefType refType)
{
return MarkResourceFrameReferenced(id, refType, ComposeFrameRefs);
}
template <typename Configuration>
void ResourceManager<Configuration>::MarkDirtyResource(ResourceId res)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(res == ResourceId())
return;
m_DirtyResources.insert(res);
}
template <typename Configuration>
bool ResourceManager<Configuration>::IsResourceDirty(ResourceId res)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(res == ResourceId())
return false;
return m_DirtyResources.find(res) != m_DirtyResources.end();
}
template <typename Configuration>
void ResourceManager<Configuration>::SetInitialContents(ResourceId id, InitialContentData contents)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
RDCASSERT(id != ResourceId());
auto it = m_InitialContents.find(id);
if(it != m_InitialContents.end())
it->second.Free(this);
m_InitialContents[id].data = contents;
}
template <typename Configuration>
void ResourceManager<Configuration>::SetInitialChunk(ResourceId id, Chunk *chunk)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
RDCASSERT(id != ResourceId());
RDCASSERT(chunk->GetChunkType<SystemChunk>() == SystemChunk::InitialContents);
InitialContentStorage &data = m_InitialContents[id];
if(data.chunk)
data.chunk->Delete();
data.chunk = chunk;
}
template <typename Configuration>
void ResourceManager<Configuration>::SetInitialFileStore(ResourceId id, const rdcstr &filename,
uint64_t start, uint64_t end)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
RDCASSERT(id != ResourceId());
RDCASSERT(!filename.empty());
RDCASSERT(end > start);
InitialContentStorage &data = m_InitialContents[id];
data.filename = filename;
data.fileStart = start;
data.fileEnd = end;
}
template <typename Configuration>
typename Configuration::InitialContentData ResourceManager<Configuration>::GetInitialContents(
ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(id == ResourceId())
return InitialContentData();
if(m_InitialContents.find(id) != m_InitialContents.end())
return m_InitialContents[id].data;
return InitialContentData();
}
// use a namespace so this doesn't pollute the global namesapce
namespace ResourceManagerInternal
{
struct WrittenRecord
{
ResourceId id;
bool written;
};
};
DECLARE_REFLECTION_STRUCT(ResourceManagerInternal::WrittenRecord);
template <class SerialiserType>
void DoSerialise(SerialiserType &ser, ResourceManagerInternal::WrittenRecord &el)
{
SERIALISE_MEMBER(id);
SERIALISE_MEMBER(written);
}
template <typename Configuration>
void ResourceManager<Configuration>::Serialise_InitialContentsNeeded(WriteSerialiser &ser)
{
using namespace ResourceManagerInternal;
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
// which resources need initial contents? either those which we didn't have proper initialisation
// for (because they were dirty one way or another) or resources that are written mid-frame and so
// need to be reset.
rdcarray<WrittenRecord> NeededInitials;
// reasonable estimate, and these records are small
NeededInitials.reserve(m_FrameReferencedResources.size() + m_InitialContents.size());
// all resources that were recorded as being modified should be included in the list of those
// needing initial contents
for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
{
RecordType *record = GetResourceRecord(it->first);
if(IsDirtyFrameRef(it->second))
{
WrittenRecord wr = {it->first, record ? record->DataInSerialiser : true};
NeededInitials.push_back(wr);
}
}
// any resources that had initial contents generated should also be included, even if they're only
// referenced read-only, as anything not in this list will have its initial contents freed on
// replay (see CreateInitialContents). However we only need to keep resources that are referenced
// (unless we have ref all resources on)
for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
{
bool include = RenderDoc::Inst().GetCaptureOptions().refAllResources;
ResourceId id = it->first;
if(m_FrameReferencedResources.find(id) != m_FrameReferencedResources.end())
include = true;
if(include)
{
WrittenRecord wr = {id, true};
NeededInitials.push_back(wr);
}
}
uint64_t chunkSize = uint64_t(NeededInitials.size() * sizeof(WrittenRecord) + 16);
SCOPED_SERIALISE_CHUNK(SystemChunk::InitialContentsList, chunkSize);
SERIALISE_ELEMENT(NeededInitials);
}
template <typename Configuration>
void ResourceManager<Configuration>::FreeInitialContents()
{
while(!m_InitialContents.empty())
{
auto it = m_InitialContents.begin();
it->second.Free(this);
if(!m_InitialContents.empty())
m_InitialContents.erase(m_InitialContents.begin());
}
m_PostponedResourceIDs.clear();
m_SkippedResourceIDs.clear();
}
template <typename Configuration>
void ResourceManager<Configuration>::Prepare_InitialStateIfPostponed(ResourceId id, bool midframe)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(!IsResourcePostponed(id))
return;
if(midframe)
{
RDCLOG("Preparing resource %s after it has been postponed.", ToStr(id).c_str());
Begin_PrepareInitialBatch();
}
WrappedResourceType res = GetCurrentResource(id);
Prepare_InitialState(res);
if(midframe)
End_PrepareInitialBatch();
m_PostponedResourceIDs.erase(id);
}
template <typename Configuration>
void ResourceManager<Configuration>::SkipOrPostponeOrPrepare_InitialState(ResourceId id,
FrameRefType refType)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(!IsResourceSkipped(id))
return;
// the first time we encounter a skipped resource, we can choose to
// skip this resource forever, convert it to be postponed, or prepare
// it immediately. We can't retrieve its initial state once it has
// been written over.
m_SkippedResourceIDs.erase(id);
// skip this forever if the first encounter is a complete write
if(IsCompleteWriteFrameRef(refType))
{
RDCDEBUG("Resource %s skipped forever on refType of %s", ToStr(id).c_str(),
ToStr(refType).c_str());
return;
}
// If this resource is only being read, we might as well try to
// postpone it to conserve memory consumption.
if(!IsDirtyFrameRef(refType) && IsResourceTrackedForPersistency(GetCurrentResource(id)))
{
m_PostponedResourceIDs.insert(id);
RDCDEBUG("Resource %s converted from skipped to postponed on refType of %s", ToStr(id).c_str(),
ToStr(refType).c_str());
SetInitialContents(id, InitialContentData());
}
else
{
WrappedResourceType res = GetCurrentResource(id);
RDCDEBUG("Preparing resource %s after it has been skipped on refType of %s", ToStr(id).c_str(),
ToStr(refType).c_str());
Begin_PrepareInitialBatch();
Prepare_InitialState(res);
End_PrepareInitialBatch();
}
}
template <typename Configuration>
inline void ResourceManager<Configuration>::ResetCaptureStartTime()
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
// This time is used to analyze which resources to refresh
// for their last write time.
m_captureStartTime = m_ResourcesUpdateTimer.GetMilliseconds();
}
template <typename Configuration>
inline void ResourceManager<Configuration>::ResetLastWriteTimes()
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
for(auto it = m_ResourceRefTimes.begin(); it != m_ResourceRefTimes.end(); ++it)
{
// Reset only those resources which were below the threshold on
// capture start. Other resource are already above the threshold.
if(m_captureStartTime - it->writeTime <= PERSISTENT_RESOURCE_AGE)
it->writeTime = m_ResourcesUpdateTimer.GetMilliseconds();
}
}
template <typename Configuration>
inline void ResourceManager<Configuration>::UpdateLastWriteTime(ResourceId id, FrameRefType refType)
{
// parent must hold m_Lock for us
// only care about write refs. A read ref would invalidate skippable state, however a skippable
// resource is left in an undefined state where reads are not valid so we don't. We need to see
// another write first before a read could be a problem, so we just pay attention for that write.
if(!IsDirtyFrameRef(refType))
return;
ResourceRefTimes *it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);
if(it == m_ResourceRefTimes.end() || it->id != id)
{
// if it's not pointing to the end, figure out where we need to insert it there
size_t idx = it - m_ResourceRefTimes.begin();
m_ResourceRefTimes.insert(idx, {id, 0.0, 0.0});
it = m_ResourceRefTimes.begin() + idx;
}
double now = m_ResourcesUpdateTimer.GetMilliseconds();
it->writeTime = now;
if(refType == eFrameRef_CompleteWriteAndDiscard)
{
// don't continually update it. We want to know that this resource *was* completely written and
// discarded, and hasn't been written in any other way since then.
if(it->firstSkipTime == 0.0)
it->firstSkipTime = now;
}
else
{
it->firstSkipTime = 0.0;
}
}
template <typename Configuration>
inline bool ResourceManager<Configuration>::HasPersistentAge(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
ResourceRefTimes *it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);
if(it == m_ResourceRefTimes.end() || it->id != id)
return true;
return m_ResourcesUpdateTimer.GetMilliseconds() - it->writeTime >= PERSISTENT_RESOURCE_AGE;
}
template <typename Configuration>
inline bool ResourceManager<Configuration>::HasSkippableAge(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
ResourceRefTimes *it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);
// if it doesn't have a write time, it can't be skippable
if(it == m_ResourceRefTimes.end() || it->id != id)
return false;
// if it's never been skipped or it was reset, it's also not skippable
if(it->firstSkipTime == 0.0)
return false;
return m_ResourcesUpdateTimer.GetMilliseconds() - it->firstSkipTime >= SKIP_RESOURCE_AGE;
}
template <typename Configuration>
inline bool ResourceManager<Configuration>::IsResourcePostponed(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
return m_PostponedResourceIDs.find(id) != m_PostponedResourceIDs.end();
}
template <typename Configuration>
inline bool ResourceManager<Configuration>::IsResourceSkipped(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
return m_SkippedResourceIDs.find(id) != m_SkippedResourceIDs.end();
}
template <typename Configuration>
inline bool ResourceManager<Configuration>::ShouldPostpone(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
WrappedResourceType res = GetCurrentResource(id);
if(!IsResourceTrackedForPersistency(res))
return false;
return HasPersistentAge(id);
}
template <typename Configuration>
inline bool ResourceManager<Configuration>::ShouldSkip(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
WrappedResourceType res = GetCurrentResource(id);
if(!IsResourceTrackedForPersistency(res))
return false;
return HasSkippableAge(id);
}
template <typename Configuration>
void ResourceManager<Configuration>::CreateInitialContents(ReadSerialiser &ser)
{
using namespace ResourceManagerInternal;
std::unordered_set<ResourceId> ids;
rdcarray<WrittenRecord> NeededInitials;
SERIALISE_ELEMENT(NeededInitials);
for(const WrittenRecord &wr : NeededInitials)
{
ResourceId id = wr.id;
ids.insert(id);
// if this resource exists and we don't have initial contents for it serialised, create some for
// reset purposes.
if(HasLiveResource(id) && m_InitialContents.find(id) == m_InitialContents.end())
Create_InitialState(id, GetLiveResource(id), wr.written);
}
// any initial contents that we ended up with which we don't need can be freed now
for(auto it = m_InitialContents.begin(); it != m_InitialContents.end();)
{
ResourceId id = it->first;
if(ids.find(id) == ids.end())
{
it->second.Free(this);
++it;
m_InitialContents.erase(id);
}
else
{
++it;
}
}
}
template <typename Configuration>
void ResourceManager<Configuration>::ApplyInitialContents()
{
RDCDEBUG("Applying initial contents");
rdcarray<ResourceId> resources = InitialContentResources();
for(auto it = resources.begin(); it != resources.end(); ++it)
{
ResourceId id = *it;
const InitialContentStorage &data = m_InitialContents[id];
WrappedResourceType live = GetLiveResource(id);
Apply_InitialState(live, data.data);
}
RDCDEBUG("Applied %d", (uint32_t)resources.size());
}
template <typename Configuration>
rdcarray<ResourceId> ResourceManager<Configuration>::InitialContentResources()
{
rdcarray<ResourceId> resources;
for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
{
ResourceId id = it->first;
if(HasLiveResource(id))
{
resources.push_back(id);
}
}
return resources;
}
template <typename Configuration>
void ResourceManager<Configuration>::MarkUnwrittenResources()
{
SCOPED_READLOCK(m_ResourceRecordLock);
for(auto it = m_ResourceRecords.begin(); it != m_ResourceRecords.end(); ++it)
it->second->MarkDataUnwritten();
}
template <typename Configuration>
void ResourceManager<Configuration>::InsertReferencedChunks(WriteSerialiser &ser)
{
std::map<int64_t, Chunk *> sortedChunks;
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
RDCDEBUG("%u frame resource records", (uint32_t)m_FrameReferencedResources.size());
if(RenderDoc::Inst().GetCaptureOptions().refAllResources)
{
SCOPED_READLOCK(m_ResourceRecordLock);
float num = float(m_ResourceRecords.size());
float idx = 0.0f;
for(auto it = m_ResourceRecords.begin(); it != m_ResourceRecords.end(); ++it)
{
RenderDoc::Inst().SetProgress(CaptureProgress::AddReferencedResources, idx / num);
idx += 1.0f;
if(m_FrameReferencedResources.find(it->first) == m_FrameReferencedResources.end() &&
it->second->InternalResource)
continue;
it->second->Insert(sortedChunks);
}
}
else
{
float num = float(m_FrameReferencedResources.size());
float idx = 0.0f;
for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
{
RenderDoc::Inst().SetProgress(CaptureProgress::AddReferencedResources, idx / num);
idx += 1.0f;
RecordType *record = GetResourceRecord(it->first);
if(record)
record->Insert(sortedChunks);
}
}
RDCDEBUG("%u frame resource chunks", (uint32_t)sortedChunks.size());
for(auto it = sortedChunks.begin(); it != sortedChunks.end(); it++)
it->second->Write(ser);
RDCDEBUG("inserted to serialiser");
}
template <typename Configuration>
void ResourceManager<Configuration>::PrepareInitialContents()
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
RDCLOG("Preparing up to %u potentially dirty resources", (uint32_t)m_DirtyResources.size());
uint32_t prepared = 0;
uint32_t postponed = 0;
uint32_t skipped = 0;
float num = float(m_DirtyResources.size());
float idx = 0.0f;
Begin_PrepareInitialBatch();
for(auto it = m_DirtyResources.begin(); it != m_DirtyResources.end(); ++it)
{
ResourceId id = *it;
RenderDoc::Inst().SetProgress(CaptureProgress::PrepareInitialStates, idx / num);
idx += 1.0f;
// if somehow this resource has been deleted but is still dirty, we can't prepare it. Resources
// deleted prior to beginning the frame capture cannot linger and be needed - we only need to
// care about resources deleted after this point (mid-capture)
if(!HasCurrentResource(id))
continue;
RecordType *record = GetResourceRecord(id);
WrappedResourceType res = GetCurrentResource(id);
// don't prepare internal resources, or those without a record
if(record == NULL || record->InternalResource)
continue;
if(ShouldSkip(id))
{
m_SkippedResourceIDs.insert(id);
skipped++;
continue;
}
if(ShouldPostpone(id))
{
m_PostponedResourceIDs.insert(id);
// Set empty contents here, it'll be prepared on serialization.
SetInitialContents(id, InitialContentData());
postponed++;
continue;
}
prepared++;
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
RDCDEBUG("Prepare Resource %s", ToStr(id).c_str());
#endif
Prepare_InitialState(res);
}
End_PrepareInitialBatch();
RDCLOG("Prepared %u dirty resources, postponed %u, skipped %u", prepared, postponed, skipped);
}
template <typename Configuration>
void ResourceManager<Configuration>::InsertInitialContentsChunks(WriteSerialiser &ser)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
uint32_t dirty = 0;
uint32_t skipped = 0;
RDCLOG("Checking %u resources with initial contents against %u referenced resources",
(uint32_t)m_InitialContents.size(), (uint32_t)m_FrameReferencedResources.size());
float num = float(m_InitialContents.size());
float idx = 0.0f;
Begin_PrepareInitialBatch();
for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
{
ResourceId id = it->first;
if(m_FrameReferencedResources.find(id) == m_FrameReferencedResources.end() &&
!RenderDoc::Inst().GetCaptureOptions().refAllResources)
{
continue;
}
RecordType *record = GetResourceRecord(id);
if(record == NULL)
continue;
if(record->InternalResource)
continue;
// Load postponed resource if needed.
Prepare_InitialStateIfPostponed(id, false);
}
End_PrepareInitialBatch();
for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
{
ResourceId id = it->first;
RenderDoc::Inst().SetProgress(CaptureProgress::SerialiseInitialStates, idx / num);
idx += 1.0f;
if(m_FrameReferencedResources.find(id) == m_FrameReferencedResources.end() &&
!RenderDoc::Inst().GetCaptureOptions().refAllResources)
{
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
RDCDEBUG("Dirty resource %s is GPU dirty but not referenced - skipping", ToStr(id).c_str());
#endif
skipped++;
continue;
}
RecordType *record = GetResourceRecord(id);
if(record == NULL)
{
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
RDCDEBUG("Resource %s has no resource record - skipping", ToStr(id).c_str());
#endif
continue;
}
if(record->InternalResource)
{
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
RDCDEBUG("Resource %s is special - skipping", ToStr(id).c_str());
#endif
continue;
}
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
RDCDEBUG("Serialising dirty Resource %s", ToStr(id).c_str());
#endif
dirty++;
if(!Need_InitialStateChunk(id, it->second.data))
{
// this was handled in ApplyInitialContentsNonChunks(), do nothing as there's no point copying
// the data again (it's already been serialised).
continue;
}
if(it->second.chunk)
{
it->second.chunk->Write(ser);
}
else if(!it->second.filename.empty())
{
FILE *f = FileIO::fopen(it->second.filename, FileIO::ReadBinary);
FileIO::fseek64(f, it->second.fileStart, SEEK_SET);
StreamReader reader(f, it->second.fileEnd - it->second.fileStart, Ownership::Stream);
StreamTransfer(ser.GetWriter(), &reader, NULL);
}
else
{
uint64_t size = GetSize_InitialState(id, it->second.data);
SCOPED_SERIALISE_CHUNK(SystemChunk::InitialContents, size);
Serialise_InitialState(ser, id, record, &it->second.data);
}
// Reset back to empty contents, unloading the actual resource.
SetInitialContents(id, InitialContentData());
}
RDCLOG("Serialised %u resources, skipped %u unreferenced", dirty, skipped);
}
template <typename Configuration>
void ResourceManager<Configuration>::ApplyInitialContentsNonChunks(WriteSerialiser &ser)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
{
ResourceId id = it->first;
if(m_FrameReferencedResources.find(id) == m_FrameReferencedResources.end() &&
!RenderDoc::Inst().GetCaptureOptions().refAllResources)
{
continue;
}
RecordType *record = GetResourceRecord(id);
if(!record || record->InternalResource)
continue;
if(!Need_InitialStateChunk(id, it->second.data))
Serialise_InitialState(ser, id, record, &it->second.data);
}
}
template <typename Configuration>
void ResourceManager<Configuration>::ClearReferencedResources()
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
{
RecordType *record = GetResourceRecord(it->first);
if(record)
{
if(IncludesWrite(it->second))
MarkDirtyResource(it->first);
record->Delete(this);
}
}
m_FrameReferencedResources.clear();
}
template <typename Configuration>
void ResourceManager<Configuration>::ReplaceResource(ResourceId from, ResourceId to)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(HasLiveResource(to))
{
m_Replacements[from] = to;
m_Replaced[to] = from;
}
}
template <typename Configuration>
bool ResourceManager<Configuration>::HasReplacement(ResourceId from)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
return m_Replacements.find(from) != m_Replacements.end();
}
template <typename Configuration>
void ResourceManager<Configuration>::RemoveReplacement(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
auto it = m_Replacements.find(id);
if(it == m_Replacements.end())
return;
m_Replaced.erase(it->second);
m_Replacements.erase(it);
}
template <typename Configuration>
typename Configuration::RecordType *ResourceManager<Configuration>::GetResourceRecord(ResourceId id)
{
SCOPED_READLOCK(m_ResourceRecordLock);
auto it = m_ResourceRecords.find(id);
if(it == m_ResourceRecords.end())
return NULL;
return it->second;
}
template <typename Configuration>
bool ResourceManager<Configuration>::HasResourceRecord(ResourceId id)
{
SCOPED_READLOCK(m_ResourceRecordLock);
auto it = m_ResourceRecords.find(id);
if(it == m_ResourceRecords.end())
return false;
return true;
}
template <typename Configuration>
typename Configuration::RecordType *ResourceManager<Configuration>::AddResourceRecord(ResourceId id)
{
SCOPED_WRITELOCK(m_ResourceRecordLock);
RDCASSERT(m_ResourceRecords.find(id) == m_ResourceRecords.end(), id);
return (m_ResourceRecords[id] = new RecordType(id));
}
template <typename Configuration>
void ResourceManager<Configuration>::RemoveResourceRecord(ResourceId id)
{
SCOPED_WRITELOCK(m_ResourceRecordLock);
RDCASSERT(m_ResourceRecords.find(id) != m_ResourceRecords.end(), id);
m_ResourceRecords.erase(id);
}
template <typename Configuration>
void ResourceManager<Configuration>::DestroyResourceRecord(ResourceRecord *record)
{
delete(RecordType *)record;
}
template <typename Configuration>
bool ResourceManager<Configuration>::AddWrapper(WrappedResourceType wrap, RealResourceType real)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
bool ret = true;
if(wrap == (WrappedResourceType)RecordType::NullResource ||
real == (RealResourceType)RecordType::NullResource)
{
RDCERR("Invalid state creating resource wrapper - wrapped or real resource is NULL");
ret = false;
}
if(m_WrapperMap[real] != (WrappedResourceType)RecordType::NullResource)
{
RDCERR("Overriding wrapper for resource");
ret = false;
}
m_WrapperMap[real] = wrap;
return ret;
}
template <typename Configuration>
void ResourceManager<Configuration>::RemoveWrapper(RealResourceType real)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(real == (RealResourceType)RecordType::NullResource || !HasWrapper(real))
{
RDCERR(
"Invalid state removing resource wrapper - real resource is NULL or doesn't have wrapper");
return;
}
m_WrapperMap.erase(m_WrapperMap.find(real));
}
template <typename Configuration>
bool ResourceManager<Configuration>::HasWrapper(RealResourceType real)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(real == (RealResourceType)RecordType::NullResource)
return false;
return (m_WrapperMap.find(real) != m_WrapperMap.end());
}
template <typename Configuration>
typename Configuration::WrappedResourceType ResourceManager<Configuration>::GetWrapper(
RealResourceType real)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(real == (RealResourceType)RecordType::NullResource)
return (WrappedResourceType)RecordType::NullResource;
if(real != (RealResourceType)RecordType::NullResource && !HasWrapper(real))
{
RDCERR(
"Invalid state removing resource wrapper - real resource isn't NULL and doesn't have "
"wrapper");
}
return m_WrapperMap[real];
}
template <typename Configuration>
void ResourceManager<Configuration>::AddLiveResource(ResourceId origid, WrappedResourceType livePtr)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(origid == ResourceId() || livePtr == (WrappedResourceType)RecordType::NullResource)
{
RDCERR("Invalid state adding resource mapping - id is invalid or live pointer is NULL");
}
m_OriginalIDs[GetID(livePtr)] = origid;
m_LiveIDs[origid] = GetID(livePtr);
if(m_LiveResourceMap.find(origid) != m_LiveResourceMap.end())
{
RDCERR("Releasing live resource for duplicate creation: %s", ToStr(origid).c_str());
ResourceTypeRelease(m_LiveResourceMap[origid]);
m_LiveResourceMap.erase(origid);
}
m_LiveResourceMap[origid] = livePtr;
}
template <typename Configuration>
bool ResourceManager<Configuration>::HasLiveResource(ResourceId origid)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(origid == ResourceId())
return false;
return (m_Replacements.find(origid) != m_Replacements.end() ||
m_LiveResourceMap.find(origid) != m_LiveResourceMap.end());
}
template <typename Configuration>
typename Configuration::WrappedResourceType ResourceManager<Configuration>::GetLiveResource(
ResourceId origid, bool optional)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(origid == ResourceId())
return (WrappedResourceType)RecordType::NullResource;
#if DISABLED(RDOC_RELEASE)
if(!optional)
{
RDCASSERT(HasLiveResource(origid), origid);
}
#endif
{
auto it = m_Replacements.find(origid);
if(it != m_Replacements.end())
return GetLiveResource(it->second);
}
{
auto it = m_LiveResourceMap.find(origid);
if(it != m_LiveResourceMap.end())
return it->second;
}
return (WrappedResourceType)RecordType::NullResource;
}
template <typename Configuration>
void ResourceManager<Configuration>::EraseLiveResource(ResourceId origid)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
RDCASSERT(HasLiveResource(origid), origid);
m_LiveResourceMap.erase(origid);
}
template <typename Configuration>
void ResourceManager<Configuration>::AddCurrentResource(ResourceId id, WrappedResourceType res)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
m_CurrentResourceMap[id] = res;
}
template <typename Configuration>
bool ResourceManager<Configuration>::HasCurrentResource(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
return m_CurrentResourceMap.find(id) != m_CurrentResourceMap.end();
}
template <typename Configuration>
typename Configuration::WrappedResourceType ResourceManager<Configuration>::GetCurrentResource(
ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
if(id == ResourceId())
return (WrappedResourceType)RecordType::NullResource;
if(m_Replacements.find(id) != m_Replacements.end())
return GetCurrentResource(m_Replacements[id]);
return m_CurrentResourceMap[id];
}
template <typename Configuration>
void ResourceManager<Configuration>::ReleaseCurrentResource(ResourceId id)
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
// We potentially need to prepare this resource on Active Capture,
// if it was postponed, but is about to go away.
if(IsActiveCapturing(m_State))
Prepare_InitialStateIfPostponed(id, true);
m_CurrentResourceMap.erase(id);
m_DirtyResources.erase(id);
auto it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);
if(it != m_ResourceRefTimes.end())
m_ResourceRefTimes.erase(it - m_ResourceRefTimes.begin());
}
template <typename Configuration>
ResourceId ResourceManager<Configuration>::GetOriginalID(ResourceId id)
{
if(id == ResourceId())
return id;
RDCASSERT(m_OriginalIDs.find(id) != m_OriginalIDs.end(), id);
return m_OriginalIDs[id];
}
template <typename Configuration>
ResourceId ResourceManager<Configuration>::GetUnreplacedOriginalID(ResourceId id)
{
if(id == ResourceId())
return id;
if(m_Replaced.find(id) != m_Replaced.end())
return m_Replaced[id];
RDCASSERT(m_OriginalIDs.find(id) != m_OriginalIDs.end(), id);
return m_OriginalIDs[id];
}
template <typename Configuration>
ResourceId ResourceManager<Configuration>::GetLiveID(ResourceId id)
{
if(id == ResourceId())
return id;
auto it = m_Replacements.find(id);
if(it != m_Replacements.end())
return it->second;
RDCASSERT(m_LiveIDs.find(id) != m_LiveIDs.end(), id);
return m_LiveIDs[id];
}