From c8a518f05cc4227fee8a572cccc1a21188dd33e8 Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 8 May 2019 14:03:04 +0100 Subject: [PATCH] Refactor rdcstr with small-string and literal-string optimisations * rdcstr no longer inherits from rdcarray, it implements all functionality itself. * There are now three representations: - Heap-allocated, same as how rdcarray behaves. - Local-allocated, for small strings we store them in a union array. - Compile-time literal, only created from user-defined literals. * The main observation is that a lot of RenderDoc's strings are compile-time literals either from struct names, member names, or stringified enum values. Storing these directly and allowing them to be moved and copied quickly saves on allocations and time. When the string is modified, it's copied to one of the other formats. --- qrenderdoc/Windows/TextureViewer.cpp | 2 +- renderdoc/api/replay/basic_types.h | 52 +- renderdoc/api/replay/rdcstr.h | 779 +++++++++++++++++++++++++ renderdoc/renderdoc.natvis | 42 +- renderdoc/renderdoc.vcxproj | 1 + renderdoc/renderdoc.vcxproj.filters | 3 + renderdoc/replay/basic_types_tests.cpp | 520 +++++++++++++++-- 7 files changed, 1275 insertions(+), 124 deletions(-) create mode 100644 renderdoc/api/replay/rdcstr.h diff --git a/qrenderdoc/Windows/TextureViewer.cpp b/qrenderdoc/Windows/TextureViewer.cpp index 0b63b4aa7..5fb4fecd2 100644 --- a/qrenderdoc/Windows/TextureViewer.cpp +++ b/qrenderdoc/Windows/TextureViewer.cpp @@ -2805,7 +2805,7 @@ void TextureViewer::OnEventChanged(uint32_t eventId) QString bindName = (copy || clear) ? tr("Destination") : QString(); QString slotName = (copy || clear) ? tr("DST") - : (m_Ctx.CurPipelineState().OutputAbbrev() + QString::number(rt)); + : QString(m_Ctx.CurPipelineState().OutputAbbrev() + QString::number(rt)); InitResourcePreview(prev, RTs[rt].resourceId, RTs[rt].typeHint, false, follow, bindName, slotName); diff --git a/renderdoc/api/replay/basic_types.h b/renderdoc/api/replay/basic_types.h index d6a409570..75dd5a146 100644 --- a/renderdoc/api/replay/basic_types.h +++ b/renderdoc/api/replay/basic_types.h @@ -767,57 +767,7 @@ public: #endif }; -DOCUMENT(""); -struct rdcstr : public rdcarray -{ - // extra string constructors - rdcstr() : rdcarray() {} - rdcstr(const rdcstr &in) : rdcarray() { assign(in); } - rdcstr(const std::string &in) : rdcarray() { assign(in.c_str(), in.size()); } - rdcstr(const char *const in) : rdcarray() { assign(in, strlen(in)); } - // extra string assignment - rdcstr &operator=(const std::string &in) - { - assign(in.c_str(), in.size()); - return *this; - } - rdcstr &operator=(const char *const in) - { - assign(in, strlen(in)); - return *this; - } - - // cast operators - operator std::string() const { return std::string(elems, elems + usedCount); } -#if defined(RENDERDOC_QT_COMPAT) - rdcstr(const QString &in) : rdcarray() - { - QByteArray arr = in.toUtf8(); - assign(arr.data(), (size_t)arr.size()); - } - operator QString() const { return QString::fromUtf8(elems, (int32_t)usedCount); } - operator QVariant() const { return QVariant(QString::fromUtf8(elems, (int32_t)usedCount)); } -#endif - - // conventional data accessor - DOCUMENT(""); - const char *c_str() const { return elems ? elems : ""; } - // equality checks - bool operator==(const char *const o) const - { - if(!elems) - return o == NULL; - return !strcmp(elems, o); - } - bool operator==(const std::string &o) const { return o == elems; } - bool operator==(const rdcstr &o) const { return *this == (const char *)o.elems; } - bool operator!=(const char *const o) const { return !(*this == o); } - bool operator!=(const std::string &o) const { return !(*this == o); } - bool operator!=(const rdcstr &o) const { return !(*this == o); } - // define ordering operators - bool operator<(const rdcstr &o) const { return strcmp(elems, o.elems) < 0; } - bool operator>(const rdcstr &o) const { return strcmp(elems, o.elems) > 0; } -}; +#include "rdcstr.h" DOCUMENT(""); struct bytebuf : public rdcarray diff --git a/renderdoc/api/replay/rdcstr.h b/renderdoc/api/replay/rdcstr.h new file mode 100644 index 000000000..eb9607748 --- /dev/null +++ b/renderdoc/api/replay/rdcstr.h @@ -0,0 +1,779 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019 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. + ******************************************************************************/ + +#pragma once + +#include +#include + +// special type for storing literals. This allows functions to force callers to pass them literals +DOCUMENT(""); +class rdcliteral +{ + const char *str; + size_t len; + + // make the literal operator a friend so it can construct fixed strings. No-one else can. + friend rdcliteral operator"" _lit(const char *str, size_t len); + + rdcliteral(const char *s, size_t l) : str(s), len(l) {} + rdcliteral() = delete; + +public: + const char *c_str() const { return str; } + size_t length() const { return len; } +}; + +inline rdcliteral operator"" _lit(const char *str, size_t len) +{ + return rdcliteral(str, len); +} + +DOCUMENT(""); +class rdcstr +{ +private: + // ARRAY_STATE is deliberately 0 so that 0-initialisation is a valid empty array string + static constexpr size_t ARRAY_STATE = size_t(0); + static constexpr size_t ALLOC_STATE = size_t(1) << ((sizeof(size_t) * 8) - 2); + static constexpr size_t FIXED_STATE = size_t(1) << ((sizeof(size_t) * 8) - 1); + + struct alloc_ptr_rep + { + // we reserve two bits but we only have three states + static constexpr size_t CAPACITY_MASK = (~size_t(0)) >> 2; + static constexpr size_t STATE_MASK = ~CAPACITY_MASK; + + // the storage + char *str; + // the current size of the string (less than or equal to capacity). Doesn't include NULL + // terminator + size_t size; + + // accessors for capacity, preserving the state bits + size_t get_capacity() const { return _capacity & CAPACITY_MASK; }; + void set_capacity(size_t s) { _capacity = ALLOC_STATE | s; } + private: + // the capacity currently available in the allocated storage. Doesn't include NULL terminator + size_t _capacity; + }; + + struct fixed_ptr_rep + { + // the immutable string storage + const char *str; + // the size of the immutable string. Doesn't include NULL terminator + size_t size; + // access to the flags + size_t flags; + }; + + struct arr_rep + { + // all bytes except the last one are used for storing short strings + char str[sizeof(size_t) * 3 - 1]; + // capacity is fixed - 1 less than the number of characters above (so we always have room for + // the NULL terminator) + static const size_t capacity = sizeof(arr_rep::str) - 1; + + // don't have to mask any state bits here because we assume the size is in bounds and state bits + // of 0 means array representation, so setting and retrieving can return the size as-is. + // We keep these accessors though just in case that changes in future + size_t get_size() const { return _size; } + void set_size(size_t s) { _size = (unsigned char)s; } + private: + // we only have 6-bits of this available is enough for up to 63 size, more than what we can + // store anyway. + unsigned char _size; + }; + + // zero-initialised this becomes an empty string in array format + union string_data + { + // stored as size, capacity, and pointer to d + alloc_ptr_rep alloc; + // stored as size and pointer + fixed_ptr_rep fixed; + // stored as in-line array + arr_rep arr; + } d; + + bool is_alloc() const { return !!(d.fixed.flags & ALLOC_STATE); } + bool is_fixed() const { return !!(d.fixed.flags & FIXED_STATE); } + bool is_array() const { return !is_alloc() && !is_fixed(); } + ///////////////////////////////////////////////////////////////// + // memory management, in a dll safe way + + DOCUMENT(""); + static char *allocate(size_t count) + { +#ifdef RENDERDOC_EXPORTS + return (char *)malloc(count); +#else + return (char *)RENDERDOC_AllocArrayMem(count); +#endif + } + static void deallocate(const char *p) + { +#ifdef RENDERDOC_EXPORTS + free((void *)p); +#else + RENDERDOC_FreeArrayMem((const void *)p); +#endif + } + + // if we're not already mutable (i.e. fixed string) then change to a mutable string + void ensure_mutable(size_t s = 0) + { + if(!is_fixed()) + return; + + // if we're not yet mutable, convert to allocated string at the same time as reserving as + // necessary + + const char *fixed_str = d.fixed.str; + size_t fixed_size = d.fixed.size; + + // allocate at least enough for the string - reserve is non-destructive. + if(s < fixed_size) + s = fixed_size; + + // if we can satisfy the request with the array representation, it's easier + if(s <= d.arr.capacity) + { + // copy d, we can safely include the NULL terminator we know is present + memcpy(d.arr.str, fixed_str, fixed_size + 1); + + // store metadata + d.arr.set_size(fixed_size); + } + else + { + // otherwise we need to allocate + + // allocate the requested size now, +1 for NULL terminator + d.alloc.str = allocate(s + 1); + // copy d, we can safely include the NULL terminator we know is present + memcpy(d.alloc.str, fixed_str, fixed_size + 1); + + // store metadata + d.alloc.set_capacity(fixed_size); + d.alloc.size = fixed_size; + } + } + +public: + // default constructor just 0-initialises + rdcstr() { memset(&d, 0, sizeof(d)); } + ~rdcstr() + { + // only free d if it was allocated + if(is_alloc()) + deallocate(d.alloc.str); + } + // move constructor is simple - just move the d element. We take ownership of the allocation if + // it's allocated, otherwise this is a copy anyway + rdcstr(rdcstr &&in) + { + // we can just move the d element + d = in.d; + + // the input no longer owns d. Set to 0 to be extra-clear + memset(&in.d, 0, sizeof(d)); + } + rdcstr &operator=(rdcstr &&in) + { + // deallocate current storage if it's allocated + if(is_alloc()) + deallocate(d.alloc.str); + + // move the d element + d = in.d; + + // the input no longer owns d. Set to 0 to be extra-clear + memset(&in.d, 0, sizeof(d)); + + return *this; + } + + // special constructor from literals + rdcstr(const rdcliteral &lit) + { + d.fixed.str = lit.c_str(); + d.fixed.size = lit.length(); + d.fixed.flags = FIXED_STATE; + } + + // copy constructors forward to assign + rdcstr(const rdcstr &in) + { + memset(&d, 0, sizeof(d)); + assign(in); + } + rdcstr(const std::string &in) + { + memset(&d, 0, sizeof(d)); + assign(in.c_str(), in.size()); + } + rdcstr(const char *const in) + { + memset(&d, 0, sizeof(d)); + assign(in, strlen(in)); + } + rdcstr(const char *const in, size_t length) + { + memset(&d, 0, sizeof(d)); + assign(in, length); + } + // also operator= + rdcstr &operator=(const rdcstr &in) + { + assign(in); + return *this; + } + rdcstr &operator=(const std::string &in) + { + assign(in.c_str(), in.size()); + return *this; + } + rdcstr &operator=(const char *const in) + { + assign(in, strlen(in)); + return *this; + } + + // assign from an rdcstr, copy the d element and allocate if needed + void assign(const rdcstr &in) + { + // if the input d is allocated, we need to make our own allocation. Go through the standard + // string assignment function which will allocate & copy + if(in.is_alloc()) + { + assign(in.d.alloc.str, in.d.alloc.size); + } + else + { + // otherwise just deallocate if necessary and copy + if(is_alloc()) + deallocate(d.alloc.str); + + d = in.d; + } + } + + // assign from something else + void assign(const char *const in, size_t length) + { + // ensure we have enough capacity allocated + reserve(length); + + // write to the string we're using, depending on if we allocated or not + char *str = is_alloc() ? d.alloc.str : d.arr.str; + // copy the string itself + memcpy(str, in, length); + // cap off with NULL terminator + str[length] = 0; + + if(is_alloc()) + d.alloc.size = length; + else + d.arr.set_size(length); + } + + void assign(const char *const str) { assign(str, strlen(str)); } + // in-place modification functions + void append(const char *const str) { append(str, strlen(str)); } + void append(const std::string &str) { append(str.c_str(), str.size()); } + void append(const rdcstr &str) { append(str.c_str(), str.size()); } + void append(const char *const str, size_t length) { insert(size(), str, length); } + void erase(size_t offs, size_t count = 1) + { + const size_t sz = size(); + + // invalid offset + if(offs >= sz) + return; + + if(count > sz - offs) + count = sz - offs; + + char *str = data(); + for(size_t i = offs; i < sz - count; i++) + str[i] = str[i + count]; + + resize(sz - count); + } + + void insert(size_t offset, const char *const str) { insert(offset, str, strlen(str)); } + void insert(size_t offset, const std::string &str) { insert(offset, str.c_str(), str.size()); } + void insert(size_t offset, const rdcstr &str) { insert(offset, str.c_str(), str.size()); } + void insert(size_t offset, const char *const instr, size_t length) + { + const size_t sz = size(); + + // invalid offset + if(offset > sz) + return; + + // allocate needed size + reserve(sz + length); + + // move anything after the offset upwards, including the NULL terminator by starting at sz + 1 + char *str = data(); + for(size_t i = sz + 1; i > offset; i--) + str[i + length - 1] = str[i - 1]; + + // copy the string to the offset + memcpy(str + offset, instr, length); + + // increase the length + if(is_alloc()) + d.alloc.size += length; + else + d.arr.set_size(sz + length); + } + + // cast operators + operator std::string() const + { + const char *s = c_str(); + return std::string(s, s + size()); + } + + // read-only by-value accessor can look up directly in c_str() since it can't be modified + char operator[](size_t i) const { return c_str()[i]; } + // assignment operator must make the string mutable first + char &operator[](size_t i) + { + ensure_mutable(); + return is_alloc() ? d.alloc.str[i] : d.arr.str[i]; + } + + // stl type interface + void reserve(size_t s) + { + if(is_fixed()) + { + ensure_mutable(s); + return; + } + + const size_t old_capacity = capacity(); + + // nothing to do if we already have this much space. We only size up + if(s <= old_capacity) + return; + + // if we're currently using the array representation, the current capacity is always maxed out, + // meaning if we don't have enough space we *must* now allocate. + + const size_t old_size = is_alloc() ? d.alloc.size : d.arr.get_size(); + const char *old_str = is_alloc() ? d.alloc.str : d.arr.str; + + // either double, or allocate what's needed, whichever is bigger. ie. by default we double in + // size but we don't grow exponentially in 2^n to cover a single really large resize + if(old_capacity * 2 > s) + s = old_capacity * 2; + + // allocate +1 for the NULL terminator + char *new_str = allocate(s + 1); + + // copy the current characters over, including NULL terminator + memcpy(new_str, old_str, old_size + 1); + + // deallocate the old storage + if(is_alloc()) + deallocate(d.alloc.str); + + // we are now an allocated string + d.alloc.str = new_str; + + // updated capacity + d.alloc.set_capacity(s); + // size is unchanged + d.alloc.size = old_size; + } + + void push_back(char c) + { + // store old size + size_t s = size(); + + // reserve enough memory and ensure we're mutable + reserve(s + 1); + + // append the character + if(is_alloc()) + { + d.alloc.size++; + d.alloc.str[s] = c; + d.alloc.str[s + 1] = 0; + } + else + { + d.arr.set_size(s + 1); + d.arr.str[s] = c; + d.arr.str[s + 1] = 0; + } + } + + void pop_back() + { + if(!empty()) + resize(size() - 1); + } + + void resize(const size_t s) + { + // if s is 0, fast path - if we're allocated just change the size, otherwise reset to an empty + // array representation. + if(s == 0) + { + if(is_alloc()) + { + d.alloc.size = 0; + d.alloc.str[0] = 0; + return; + } + else + { + // either we're a fixed string, and we need to become an empty array, or we're already an + // array in which case we empty the array. + memset(&d, 0, sizeof(d)); + return; + } + } + + const size_t oldSize = size(); + + // call reserve first. This handles resizing up, and also making the string mutable if necessary + reserve(s); + + // if the size didn't change, return. + if(s == oldSize) + return; + + // now resize the string + if(is_alloc()) + { + // if we resized upwards, memset the new elements to 0, if we resized down set the new NULL + // terminator + if(s > oldSize) + memset(d.alloc.str + oldSize, 0, s - oldSize + 1); + else + d.alloc.str[s] = 0; + + // update the size. + d.alloc.size = s; + } + else + { + // if we resized upwards, memset the new elements to 0, if we resized down set the new NULL + // terminator + if(s > oldSize) + memset(d.arr.str + oldSize, 0, s - oldSize + 1); + else + d.arr.str[s] = 0; + + // update the size. + d.arr.set_size(s); + } + } + + size_t capacity() const + { + if(is_alloc()) + return d.alloc.get_capacity(); + if(is_fixed()) + return d.fixed.size; + return d.arr.capacity; + } + size_t size() const + { + if(is_alloc() || is_fixed()) + return d.fixed.size; + return d.arr.get_size(); + } + size_t length() const { return size(); } + const char *c_str() const + { + if(is_alloc() || is_fixed()) + return d.alloc.str; + return d.arr.str; + } + + void clear() { resize(0); } + bool empty() const { return size() == 0; } + const char *data() const { return c_str(); } + char *data() + { + ensure_mutable(); + return is_alloc() ? d.alloc.str : d.arr.str; + } + const char *begin() const { return c_str(); } + const char *end() const { return c_str() + size(); } + char front() const { return *c_str(); } + char &front() + { + ensure_mutable(); + return data()[0]; + } + char back() const { return *(end() - 1); } + char &back() + { + ensure_mutable(); + return data()[size() - 1]; + } + + rdcstr substr(size_t offs, size_t length = ~0U) + { + const size_t sz = size(); + if(offs >= sz) + return rdcstr(); + + if(length == ~0U || offs + length > sz) + length = sz - offs; + + return rdcstr(c_str() + offs, length); + } + + rdcstr &operator+=(const char *const str) + { + append(str, strlen(str)); + return *this; + } + rdcstr &operator+=(const std::string &str) + { + append(str.c_str(), str.size()); + return *this; + } + rdcstr &operator+=(const rdcstr &str) + { + append(str.c_str(), str.size()); + return *this; + } + rdcstr operator+(const char *const str) const + { + rdcstr ret = *this; + ret += str; + return ret; + } + rdcstr operator+(const std::string &str) const + { + rdcstr ret = *this; + ret += str; + return ret; + } + rdcstr operator+(const rdcstr &str) const + { + rdcstr ret = *this; + ret += str; + return ret; + } + + // Qt-type interface + bool isEmpty() const { return size() == 0; } + int32_t count() const { return (int32_t)size(); } + char takeAt(size_t offs) + { + char ret = c_str()[offs]; + erase(offs); + return ret; + } + + // Python interface + int32_t indexOf(char el, size_t first = 0, size_t last = ~0U) const + { + const char *str = c_str(); + const size_t sz = size(); + + for(size_t i = first; i < sz && i < last; i++) + { + if(str[i] == el) + return (int32_t)i; + } + + return -1; + } + + int32_t find(const char *needle_str, size_t needle_len, size_t first = 0, size_t last = ~0U) const + { + const char *haystack = c_str(); + const size_t haystack_len = size(); + + if(needle_len > haystack_len) + return -1; + + if(needle_len == 0) + return 0; + + for(size_t i = first; i <= haystack_len - needle_len && i < last; i++) + { + if(strncmp(haystack + i, needle_str, needle_len) == 0) + return (int32_t)i; + } + + return -1; + } + + int32_t find(const rdcstr &needle, size_t first = 0, size_t last = ~0U) const + { + return find(needle.c_str(), needle.size(), first, last); + } + int32_t find(const std::string &needle, size_t first = 0, size_t last = ~0U) const + { + return find(needle.c_str(), needle.size(), first, last); + } + int32_t find(const char *needle, size_t first = 0, size_t last = ~0U) const + { + return find(needle, strlen(needle), first, last); + } + + bool contains(char needle) const { return indexOf(needle) != -1; } + bool contains(const rdcstr &needle) const { return find(needle) != -1; } + bool contains(const std::string &needle) const { return find(needle) != -1; } + bool contains(const char *needle) const { return find(needle) != -1; } + void removeOne(char el) + { + int idx = indexOf(el); + if(idx >= 0) + erase((size_t)idx); + } + + // for equality check with rdcstr, check quickly for empty string comparisons + bool operator==(const rdcstr &o) const + { + if(o.size() == 0) + return size() == 0; + return !strcmp(o.c_str(), c_str()); + } + + // equality checks for other types, just check string directly + bool operator==(const char *const o) const + { + if(o == NULL) + return size() == 0; + return !strcmp(o, c_str()); + } + bool operator==(const std::string &o) const { return o == c_str(); } + // for inverse check just reverse results of above + bool operator!=(const char *const o) const { return !(*this == o); } + bool operator!=(const std::string &o) const { return !(*this == o); } + bool operator!=(const rdcstr &o) const { return !(*this == o); } + // define ordering operators + bool operator<(const rdcstr &o) const { return strcmp(c_str(), o.c_str()) < 0; } + bool operator>(const rdcstr &o) const { return strcmp(c_str(), o.c_str()) > 0; } +// Qt compatibility +#if defined(RENDERDOC_QT_COMPAT) + rdcstr(const QString &in) + { + QByteArray arr = in.toUtf8(); + memset(&d, 0, sizeof(d)); + assign(arr.data(), (size_t)arr.size()); + } + rdcstr(const QChar &in) + { + QByteArray arr = QString(in).toUtf8(); + memset(&d, 0, sizeof(d)); + assign(arr.data(), (size_t)arr.size()); + } + operator QString() const { return QString::fromUtf8(c_str(), (int32_t)size()); } + operator QVariant() const { return QVariant(QString::fromUtf8(c_str(), (int32_t)size())); } + rdcstr &operator+=(const QString &str) + { + QByteArray arr = str.toUtf8(); + append(arr.data(), (size_t)arr.size()); + return *this; + } + rdcstr operator+(const QString &str) const + { + rdcstr ret = *this; + ret += str; + return ret; + } + rdcstr &operator+=(const QChar &chr) + { + QByteArray arr = QString(chr).toUtf8(); + append(arr.data(), (size_t)arr.size()); + return *this; + } + rdcstr operator+(const QChar &chr) const + { + rdcstr ret = *this; + ret += QString(chr); + return ret; + } +#endif +}; + +// macro that can append _lit to a macro parameter +#define STRING_LITERAL2(string) string##_lit +#define STRING_LITERAL(string) STRING_LITERAL2(string) + +inline rdcstr operator+(const char *const left, const rdcstr &right) +{ + return rdcstr(left) += right; +} + +inline rdcstr operator+(const std::string &left, const rdcstr &right) +{ + return rdcstr(left) += right; +} + +inline bool operator==(const char *const left, const rdcstr &right) +{ + return right == left; +} + +inline bool operator==(const std::string &left, const rdcstr &right) +{ + return right == left; +} + +inline bool operator!=(const char *const left, const rdcstr &right) +{ + return right != left; +} + +inline bool operator!=(const std::string &left, const rdcstr &right) +{ + return right != left; +} + +inline std::ostream &operator<<(std::ostream &os, rdcstr const &str) +{ + return os << str.c_str(); +} + +#if defined(RENDERDOC_QT_COMPAT) +inline rdcstr operator+(const QString &left, const rdcstr &right) +{ + return rdcstr(left) += right; +} + +inline rdcstr operator+(const QChar &left, const rdcstr &right) +{ + return rdcstr(left) += right; +} +#endif \ No newline at end of file diff --git a/renderdoc/renderdoc.natvis b/renderdoc/renderdoc.natvis index 2180819ff..acbbefcd0 100644 --- a/renderdoc/renderdoc.natvis +++ b/renderdoc/renderdoc.natvis @@ -1,15 +1,41 @@ - "" - {elems,s} - elems,s + {d.fixed.str,s} + {d.alloc.str,s} + {d.arr.str,s} + d.fixed.str,s + d.alloc.str,s + d.arr.str,s - usedCount - allocatedCount - - usedCount - elems + + Compile-Time Literal + + d.fixed.size + "N/A" + + d.fixed.size + d.fixed.str + + + + Heap-allocated String + + d.alloc.size + d.alloc._capacity + + d.alloc.size + d.alloc.str + + + + Internal Array Small String + + d.arr._size + d.arr.capacity + + d.arr._size + d.arr.str diff --git a/renderdoc/renderdoc.vcxproj b/renderdoc/renderdoc.vcxproj index b0fefec01..6a1668ec2 100644 --- a/renderdoc/renderdoc.vcxproj +++ b/renderdoc/renderdoc.vcxproj @@ -166,6 +166,7 @@ + diff --git a/renderdoc/renderdoc.vcxproj.filters b/renderdoc/renderdoc.vcxproj.filters index e58aea478..231074b68 100644 --- a/renderdoc/renderdoc.vcxproj.filters +++ b/renderdoc/renderdoc.vcxproj.filters @@ -477,6 +477,9 @@ Resources\glsl + + API\Replay + diff --git a/renderdoc/replay/basic_types_tests.cpp b/renderdoc/replay/basic_types_tests.cpp index 9ae3aab6b..841573fdb 100644 --- a/renderdoc/replay/basic_types_tests.cpp +++ b/renderdoc/replay/basic_types_tests.cpp @@ -24,6 +24,7 @@ #include "api/replay/renderdoc_replay.h" #include "common/globalconfig.h" +#include "common/timing.h" #include "os/os_specific.h" #if ENABLED(ENABLE_UNIT_TESTS) @@ -569,78 +570,469 @@ TEST_CASE("Test array type", "[basictypes]") }; #define CHECK_NULL_TERM(str) CHECK(str.c_str()[str.size()] == '\0'); +#define SMALL_STRING "Small str!" +#define LARGE_STRING \ + "String literal that cannot be stored directly in a small-string optimisation array!" +#define VERY_LARGE_STRING \ + R"(So: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce viverra dui dolor. Donec fermentum metus eu lorem rutrum, nec sodales urna vehicula. Praesent finibus tincidunt volutpat. Aliquam ullamcorper metus semper suscipit dignissim. Phasellus at odio nec arcu venenatis euismod id eget mi. Vestibulum consequat nisi sed massa venenatis, vel pellentesque nunc semper. Maecenas porttitor nulla non purus pellentesque pharetra. Ut ornare rhoncus massa at eleifend. Sed ultricies tincidunt bibendum. Pellentesque neque dolor, elementum eget scelerisque et, euismod at tortor. Duis vel porta sapien. Integer facilisis nisl condimentum tempor faucibus. Sed convallis tempus dolor quis fringilla. Nam dictum accumsan quam, eget pretium turpis mattis id. Praesent vitae enim ut est porttitor consectetur et at ante. Proin porttitor quam eu enim gravida, eget congue diam dapibus.!)" TEST_CASE("Test string type", "[basictypes][string]") { - rdcstr test; + RDCCOMPILE_ASSERT(sizeof(rdcstr) == sizeof(void *) * 3, "rdcstr is mis-sized"); - // should not have any data in it - CHECK(test.size() == 0); - CHECK(test.capacity() == 0); - CHECK(test.empty()); - CHECK(test.isEmpty()); - CHECK(test.begin() == test.end()); - - CHECK(test.c_str() != NULL); - CHECK_NULL_TERM(test); - - test = "Test string type"; - - CHECK(test.size() == 16); - CHECK(test.capacity() >= 16); - CHECK_FALSE(test.empty()); - CHECK_FALSE(test.isEmpty()); - CHECK(test.begin() + 16 == test.end()); - - CHECK(strlen(test.c_str()) == 16); - CHECK(test.c_str() != NULL); - CHECK(test == "Test string type"); - CHECK(test == std::string("Test string type")); - CHECK(test == rdcstr("Test string type")); - CHECK_NULL_TERM(test); - - test[4] = '!'; - - CHECK(test.size() == 16); - CHECK(test.capacity() >= 16); - CHECK_FALSE(test.empty()); - CHECK_FALSE(test.isEmpty()); - CHECK(test.begin() + 16 == test.end()); - - CHECK(strlen(test.c_str()) == 16); - CHECK(test.c_str() != NULL); - CHECK(test == "Test!string type"); - CHECK(test == std::string("Test!string type")); - CHECK(test == rdcstr("Test!string type")); - CHECK_NULL_TERM(test); - - test.clear(); - - CHECK(test.size() == 0); - CHECK(test.capacity() >= 16); - CHECK(test.empty()); - CHECK(test.isEmpty()); - CHECK(test.begin() == test.end()); - CHECK_NULL_TERM(test); - - rdcstr test2; - - test2 = test; - - CHECK(test.size() == test2.size()); - CHECK(test.empty() == test2.empty()); - - for(size_t i = 0; i < test.size(); i++) + SECTION("Empty strings") { - CHECK(test[i] == test2[i]); - } + const rdcstr test; - rdcstr empty; + // should not have any data in it + CHECK(test.size() == 0); + CHECK(test.empty()); + CHECK(test.isEmpty()); + CHECK(test.begin() == test.end()); - test2 = empty; + CHECK(test.c_str() != NULL); + CHECK_NULL_TERM(test); + CHECK(test == ""); + CHECK(test == ((const char *)NULL)); + CHECK(test == rdcstr()); + CHECK(test == std::string()); + }; - CHECK(test.size() == empty.size()); - CHECK(test.empty() == empty.empty()); + SECTION("Empty string after containing data") + { + rdcstr test; + + auto lambda = [](rdcstr test, const char *str) { + test.clear(); + + CHECK(test.size() == 0); + CHECK(test.empty()); + CHECK(test.isEmpty()); + CHECK(test.begin() == test.end()); + + CHECK(test.c_str() != NULL); + CHECK_NULL_TERM(test); + }; + + lambda(SMALL_STRING, SMALL_STRING); + lambda(LARGE_STRING, LARGE_STRING); + lambda(VERY_LARGE_STRING, VERY_LARGE_STRING); + lambda(STRING_LITERAL(LARGE_STRING), LARGE_STRING); + }; + + SECTION("Small string read-only accessors") + { + auto lambda = [](const rdcstr &test, const char *str) { + const size_t len = strlen(str); + + CHECK(test.size() == len); + CHECK(test.capacity() >= len); + CHECK_FALSE(test.empty()); + CHECK_FALSE(test.isEmpty()); + CHECK(test.begin() + len == test.end()); + + CHECK(strlen(test.c_str()) == len); + CHECK(test.c_str() != NULL); + CHECK(strcmp(test.c_str(), str) == 0); + CHECK(test != ((const char *)NULL)); + CHECK(test == str); + CHECK(test == std::string(str)); + CHECK(test == rdcstr(str)); + CHECK_NULL_TERM(test); + + CHECK(test.front() == 'S'); + CHECK(test.back() == '!'); + }; + + lambda(SMALL_STRING, SMALL_STRING); + lambda(LARGE_STRING, LARGE_STRING); + lambda(VERY_LARGE_STRING, VERY_LARGE_STRING); + lambda(STRING_LITERAL(LARGE_STRING), LARGE_STRING); + }; + + SECTION("String read-only accessors after modification") + { + auto lambda = [](rdcstr test, const char *str) { + const size_t len = strlen(str); + + test[4] = '!'; + + CHECK(test.size() == len); + CHECK(test.capacity() >= len); + CHECK_FALSE(test.empty()); + CHECK_FALSE(test.isEmpty()); + CHECK(test.begin() + len == test.end()); + + CHECK(strlen(test.c_str()) == len); + CHECK(test.c_str() != NULL); + CHECK(strcmp(test.c_str(), str) == -1); + CHECK(test != str); + CHECK(test != std::string(str)); + CHECK(test != rdcstr(str)); + CHECK_NULL_TERM(test); + + CHECK(test.front() == 'S'); + CHECK(test.back() == '!'); + }; + + lambda(SMALL_STRING, SMALL_STRING); + lambda(LARGE_STRING, LARGE_STRING); + lambda(VERY_LARGE_STRING, VERY_LARGE_STRING); + lambda(STRING_LITERAL(LARGE_STRING), LARGE_STRING); + }; + + SECTION("String copies") + { + auto lambda = [](const rdcstr &test, const char *str) { + rdcstr test2; + + test2 = test; + + CHECK(test.size() == test2.size()); + CHECK(test.empty() == test2.empty()); + + for(size_t i = 0; i < test.size(); i++) + { + CHECK(test[i] == test2[i]); + } + + CHECK(test.c_str() != test2.c_str()); + + rdcstr empty; + + test2 = empty; + + CHECK(test2.size() == empty.size()); + CHECK(test2.empty() == empty.empty()); + }; + + lambda(SMALL_STRING, SMALL_STRING); + lambda(LARGE_STRING, LARGE_STRING); + lambda(VERY_LARGE_STRING, VERY_LARGE_STRING); + lambda(STRING_LITERAL(LARGE_STRING), LARGE_STRING); + }; + + SECTION("Shrinking and expanding strings") + { + rdcstr test = "A longer string that would have been heap allocated"; + test.resize(5); + + CHECK(test.size() == 5); + CHECK_NULL_TERM(test); + CHECK(test == "A lon"); + + // should do nothing + test.resize(5); + + CHECK(test.size() == 5); + CHECK_NULL_TERM(test); + CHECK(test == "A lon"); + + // this copy will copy to the internal array since it's small enough now + rdcstr test2 = test; + + CHECK(test2.size() == 5); + CHECK_NULL_TERM(test2); + CHECK(test2 == "A lon"); + + test2 = "abcdefghij"; + + CHECK(test2.size() == 10); + CHECK_NULL_TERM(test2); + + test2.resize(3); + + CHECK(test2.size() == 3); + CHECK_NULL_TERM(test2); + + test2.resize(6); + + CHECK(test2.size() == 6); + CHECK_NULL_TERM(test2); + + test.resize(12345); + + CHECK(test.capacity() == 12345); + CHECK(test.size() == 12345); + + const char *prev_ptr = test.c_str(); + + // this could fit in the internal array but to avoid allocation thrashing we should keep the + // same allocation + test = "Short str"; + + CHECK(test.capacity() == 12345); + CHECK(test.size() == 9); + CHECK(test.c_str() == prev_ptr); + CHECK_NULL_TERM(test); + CHECK(test == "Short str"); + + test.resize(4); + + CHECK(test.size() == 4); + CHECK_NULL_TERM(test); + CHECK(test == "Shor"); + + test.resize(8); + + CHECK(test.size() == 8); + CHECK_NULL_TERM(test); + CHECK(test == "Shor"); + CHECK(test[4] == 0); + CHECK(test[5] == 0); + CHECK(test[6] == 0); + CHECK(test[7] == 0); + }; + + SECTION("erase") + { + rdcstr test = "Hello, World! This is a test string"; + + test.erase(0); + + CHECK(test == "ello, World! This is a test string"); + + test.erase(0, 4); + + CHECK(test == ", World! This is a test string"); + + test.erase(9, 5); + + CHECK(test == ", World! is a test string"); + + test.erase(14, 1000); + + CHECK(test == ", World! is a "); + + test.erase(100); + + CHECK(test == ", World! is a "); + + test.erase(100, 100); + + CHECK(test == ", World! is a "); + }; + + SECTION("append") + { + rdcstr test = "Hello"; + + test += " World"; + + CHECK(test.size() == 11); + CHECK_NULL_TERM(test); + CHECK(test == "Hello World"); + + rdcstr test2 = test + "!"; + + CHECK(test2.size() == 12); + CHECK_NULL_TERM(test2); + CHECK(test2 == "Hello World!"); + + test2 += " And enough characters to force an allocation"; + + CHECK(test2 == "Hello World! And enough characters to force an allocation"); + + test2 += ", " + test + "?"; + + CHECK(test2 == "Hello World! And enough characters to force an allocation, Hello World?"); + }; + + SECTION("insert") + { + rdcstr test = "Hello World!"; + + test.insert(5, ","); + + CHECK(test == "Hello, World!"); + + rdcstr test2 = test; + + test2.insert(0, test); + + CHECK(test2 == "Hello, World!Hello, World!"); + + test2.insert(100, "foo"); + + CHECK(test2 == "Hello, World!Hello, World!"); + }; + + SECTION("push_back and pop_back") + { + rdcstr test = "Hello, World!"; + + test.push_back('!'); + + CHECK(test == "Hello, World!!"); + + test.push_back('!'); + + CHECK(test == "Hello, World!!!"); + + test.pop_back(); + + CHECK(test == "Hello, World!!"); + + test.pop_back(); + + CHECK(test == "Hello, World!"); + + test.pop_back(); + + CHECK(test == "Hello, World"); + + test.clear(); + + CHECK(test == ""); + + test.pop_back(); + + CHECK(test == ""); + + test = "Longer string to force a heap allocation: Hello, World!"; + + test.push_back('!'); + + CHECK(test == "Longer string to force a heap allocation: Hello, World!!"); + + test.pop_back(); + + CHECK(test == "Longer string to force a heap allocation: Hello, World!"); + + test.pop_back(); + + CHECK(test == "Longer string to force a heap allocation: Hello, World"); + + test.clear(); + + CHECK(test == ""); + + test.pop_back(); + + CHECK(test == ""); + }; + + SECTION("substr") + { + rdcstr test = "Hello, World!"; + + CHECK(test.substr(0) == "Hello, World!"); + CHECK(test.substr(1) == "ello, World!"); + CHECK(test.substr(5) == ", World!"); + CHECK(test.substr(13) == ""); + CHECK(test.substr(100) == ""); + CHECK(test.substr(5, 2) == ", "); + CHECK(test.substr(5, 100) == ", World!"); + + test = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!"; + + CHECK(test.substr(0) == + "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(1) == "ello, World! Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(5) == ", World! Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(13) == " Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(69) == ""); + CHECK(test.substr(100) == ""); + CHECK(test.substr(5, 2) == ", "); + CHECK(test.substr(5, 100) == + ", World! Hello, World! Hello, World! Hello, World! Hello, World!"); + + test = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!"_lit; + + CHECK(test.substr(0) == + "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(1) == "ello, World! Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(5) == ", World! Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(13) == " Hello, World! Hello, World! Hello, World! Hello, World!"); + CHECK(test.substr(69) == ""); + CHECK(test.substr(100) == ""); + CHECK(test.substr(5, 2) == ", "); + CHECK(test.substr(5, 100) == + ", World! Hello, World! Hello, World! Hello, World! Hello, World!"); + }; + + SECTION("searching") + { + rdcstr test = "Hello, World!"; + + CHECK(test.find("Hello") == 0); + CHECK(test.find("World") == 7); + CHECK(test.find("ld!") == 10); + CHECK(test.find("Foobar") == -1); + CHECK(test.find("Hello, World!!") == -1); + CHECK(test.find("Hello, World?") == -1); + CHECK(test.find("") == 0); + + CHECK(test.indexOf('H') == 0); + CHECK(test.indexOf('l') == 2); + CHECK(test.indexOf('?') == -1); + + CHECK(test.contains('!')); + CHECK_FALSE(test.contains('?')); + + CHECK(test.contains('H')); + CHECK(test.contains("Hello")); + + char H = test.takeAt(0); + + CHECK(H == 'H'); + CHECK_FALSE(test.contains('H')); + CHECK_FALSE(test.contains("Hello")); + + test.removeOne('!'); + + CHECK_FALSE(test.contains('!')); + + CHECK(test == "ello, World"); + }; + + SECTION("String literal tests") + { + rdcstr test = STRING_LITERAL(LARGE_STRING); + const size_t len = strlen(LARGE_STRING); + + CHECK(test.size() == len); + CHECK(test.capacity() == test.size()); + CHECK(strlen(test.c_str()) == test.size()); + + rdcstr test2; + + test2.resize(12345); + test2 = test; + + CHECK(test2.size() == len); + CHECK(test2.capacity() == test2.size()); + + CHECK(test == test2); + + // should both be pointing directly to the string storage, so identical pointers + CHECK(test.c_str() == test2.c_str()); + + test2.reserve(1); + + // they will be equal still but not with the same storage now + CHECK(test == test2); + CHECK(test.c_str() != test2.c_str()); + + test2[0] = '!'; + + CHECK(test != test2); + + test = test2; + + // equal now but still not with the same storage + CHECK(test == test2); + CHECK(test.c_str() != test2.c_str()); + + test = "short literal"_lit; + test2 = test; + + // this does the copy-on-write but into the internal array + test[0] = 'S'; + + CHECK(test == "Short literal"); + CHECK(test.size() == test2.size()); + }; }; #endif // ENABLED(ENABLE_UNIT_TESTS) \ No newline at end of file