diff options
| author | Theresa Foley <10618364+tangent-vector@users.noreply.github.com> | 2025-08-06 19:30:57 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-07 02:30:57 +0000 |
| commit | 631a4c37a597fab7599abc472f25d671bfe37751 (patch) | |
| tree | 799e3cc30021238cea6d6738338876b154b0f5e3 /source/slang | |
| parent | b9aad6649e3ba0186e17fc035d4b56f8e6169b59 (diff) | |
Improve performance of AST deserialization (#7935)
* Improve performance of AST deserialization
The primary goal of these changes is to reduce the total time spent in the global session's `loadBuiltinModule()`, which gets called as part of global session creation to load the core module, and thus impacts every invocation of `slangc` and every user of the Slang compiler API.
The majority of the time is spent simply deserializing the core module's AST and IR and, of those two, the AST takes significantly longer to load than the IR (in the ballpark of 5x the time).
This change is focused on the serialization infrastructure but, given the performance situation described above, the focus is first and foremost on *deserialization* performance for the Slang *AST*, when using the *fossil* format.
That focus shows through in the changes that have been implemented.
Change serialization framework to use `template` instead of `virtual`
=====================================================================
The recently-introduced serialization framework in `slang-serialize.h` was centered around a dynamically-dispatched `ISerializerImpl` interface.
As a result, every single invocation of a `serialize(...)` call ultimately went through `virtual` function dispatch.
While the overhead of the `virtual` calls themselves does not have a major impact on the total deserialization performance, those calls end up serving as a barrier to further optimization.
This change changes operations that used to take a `Serializer const&` (which wraps an `ISerializerImpl*`), to instead declare a template parameter `<typename S>` and take an `S const&`.
The main consequence of the change is that `serialize()` functions for user-defined types will need to be template functions, and thus either be defined in headers (alongside the type that they serialize) or else in the specific source file that handles serialization (as is currently being done for the AST-related types in `slang-serialize-ast.cpp`).
Note that if we later decide that we want the ability to perform serialization through a dynamically-dispatched interface (e.g., to easily toggle between different serialization back-ends), it will be easier to layer a dynamically-dispatched implementation on top of the statically-dispatched `template` version than the other way around.
Generous use of `SLANG_FORCE_INLINE`
====================================
In order to unlock further optimizations, a bunch of operations were marked with `SLANG_FORCE_INLINE`.
It is important to note that forcing inlining like this is a big hammer, and needs to be approached with at least a little caution.
The simplest cases are:
* trivial wrapper function that just delegate to another function
* functions that only have a single call site (but exist to keep abstractions clean)
Externalize Scope for `begin`/`end` Operations
==============================================
The old `ISerializerImpl` interface had a bunch of paired begin/end operations that define the hierarchical structure of data being read.
Most serializer implementations (whether for reading or writing) use these operations to help maintain some kind of internal stack for tracking state in the hierarchy.
The overhead of maintaining such a stack with something like a `List<T>` amortizes out over many operations, but even that overhead is unnecessary when the begin/end pairs are *already* mirroring the call stack of the code invoking serialization.
This change modifies the `ScopedSerializerFoo` types so that they each provide a piece of stack-allocated storage to the serializer back-end's `beginFoo()` and `endFoo()` operations.
Currently only the `Fossil::SerialReader` is making use of that facility, but the other implementations of readers and writers in the codebase could be adapted if we ever wanted to.
Streamline `Fossil::SerialReader`
=================================
The most significant performance gains came from changes to the `Fossil::SerialReader` type, aimed at minimizing the cycles spent in the core `_readValPtr()` routine.
That function used to have a large-ish `switch` statement that implemented superficially very different reading logic depending on the outer container/object being read from.
The new logic pushes more work back on the `begin` and `end` operations (which get invoked far less frequently than simple scalar/pointer values get read), so that they always set up the state of the reader with direct pointers to the data and layout for the next fossilized value to be read.
The remaining work in `_readValPtr()` has been factored into a differnt subroutine - `_advanceCursor()` - that takes responsibility for advancing the data pointer, and updating the various other fields.
The `_advanceCursor()` routine is still messier than is ideal, because it has to deal with the various different kinds of logic required for navigating to the next value.
Various other conditionals inside the `SerialReader` implementation were streamlined, mostly by collapsing the `State::Type` enumeration down to only represent the cases that are truly semantically distinct.
Evaluated: Streamline Layout Rules for Fossil
=============================================
One potential approach that I implemented but then reverted (after finding it had little to no performance impact) was changing the fossil format to always write things with 4-byte alignment/granularity.
That would mean values smaller than 4 bytes would get inflated to a full 4 bytes, and scalar values larger than 4 bytes get written with only 4-byte alignment (requiring unaligned loads to read them).
I found that the only way to take advantage of the simplified layout rules to improve read performance would be to more-or-less eliminate the use of the layout information embedded in the fossil data, which would make it very difficult to validate that the data is correctly structured.
Possible Future Work: Further Type Specialization
=================================================
As it stands, the biggest overhead remaining on the critical path of `_readValPtr()` is the way the `_advanceCursor()` logic needs to take different approaches depending on the type of the surrounding context (advancing through elements of a container is very different than advancing through fields of a `struct`, for example).
The interesting thing to note is that at the use site within a `serialize()` function, it is usually manifestly obvious which case something is in.
If the code uses `SLANG_SCOPED_SERIALIZER_ARRAY` it is in a container, while if it uses `SLANG_SCOPED_SERIALIZER_STRUCT` it is in a struct.
This means that the contextual information is staticaly available, but just isn't exposed in a way that lets the core reading logic take advantage of it.
A logical extension of the work here would be to expand on the `Scope` idea added in this change such that most of the serialization operations (`handleInt32`, `handleString`, etc.) are actually dispatched through the scope, and then have each of the `SLANG_SCOPED_SERIALIZER_...` macros instantiate a *different* scope type (still dependent on the serializer).
* fixup
* format code
* typo
---------
Co-authored-by: slangbot <186143334+slangbot@users.noreply.github.com>
Diffstat (limited to 'source/slang')
| -rw-r--r-- | source/slang/slang-fossil.cpp | 36 | ||||
| -rw-r--r-- | source/slang/slang-fossil.h | 185 | ||||
| -rw-r--r-- | source/slang/slang-serialize-ast.cpp | 166 | ||||
| -rw-r--r-- | source/slang/slang-serialize-fossil.cpp | 532 | ||||
| -rw-r--r-- | source/slang/slang-serialize-fossil.h | 483 | ||||
| -rw-r--r-- | source/slang/slang-serialize-ir.cpp | 11 | ||||
| -rw-r--r-- | source/slang/slang-serialize-riff.cpp | 69 | ||||
| -rw-r--r-- | source/slang/slang-serialize-riff.h | 144 | ||||
| -rw-r--r-- | source/slang/slang-serialize.h | 422 |
9 files changed, 1194 insertions, 854 deletions
diff --git a/source/slang/slang-fossil.cpp b/source/slang/slang-fossil.cpp index 204430901..b5074eac6 100644 --- a/source/slang/slang-fossil.cpp +++ b/source/slang/slang-fossil.cpp @@ -77,30 +77,6 @@ Fossil::AnyValPtr getRootValue(void const* data, Size size) } // namespace Fossil -Size FossilizedStringObj::getSize() const -{ - auto sizePtr = (FossilUInt*)this - 1; - return Size(*sizePtr); -} - -UnownedTerminatedStringSlice FossilizedStringObj::get() const -{ - auto size = getSize(); - return UnownedTerminatedStringSlice((char*)this, size); -} - -Count FossilizedContainerObjBase::getElementCount() const -{ - auto countPtr = (FossilUInt*)this - 1; - return Size(*countPtr); -} - -FossilizedValLayout* FossilizedVariantObj::getContentLayout() const -{ - auto layoutPtrPtr = (FossilizedPtr<FossilizedValLayout>*)this - 1; - return (*layoutPtrPtr).get(); -} - Fossil::AnyValRef Fossil::ValRef<FossilizedContainerObjBase>::getElement(Index index) const { SLANG_ASSERT(index >= 0); @@ -137,16 +113,4 @@ Fossil::AnyValRef Fossil::ValRef<FossilizedRecordVal>::getField(Index index) con return Fossil::AnyValRef(fieldPtr, fieldInfo->layout); } -#if 0 -FossilizedValRef getVariantContent(FossilizedVariantObjRef variantRef) -{ - return getVariantContent(variantRef.getData()); -} -#endif - -Fossil::AnyValPtr getVariantContentPtr(FossilizedVariantObj* variantPtr) -{ - return Fossil::AnyValPtr(variantPtr->getContentDataPtr(), variantPtr->getContentLayout()); -} - } // namespace Slang diff --git a/source/slang/slang-fossil.h b/source/slang/slang-fossil.h index 8d2465ddb..b5fd9b7b5 100644 --- a/source/slang/slang-fossil.h +++ b/source/slang/slang-fossil.h @@ -69,7 +69,10 @@ public: using Layout = FossilizedPtrLikeLayout; - static bool isMatchingKind(FossilizedValKind kind) { return kind == FossilizedValKind::Ptr; } + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) + { + return kind == FossilizedValKind::Ptr; + } }; static_assert(sizeof(FossilizedPtr<void>) == sizeof(uint32_t)); @@ -150,11 +153,11 @@ public: using Layout = FossilizedValLayout; static const FossilizedValKind kKind = Kind; - T const& get() const { return _value; } + SLANG_FORCE_INLINE T const& get() const { return _value; } - operator T const&() const { return _value; } + SLANG_FORCE_INLINE operator T const&() const { return _value; } - static bool isMatchingKind(FossilizedValKind kind) { return kind == kKind; } + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == kKind; } private: T _value; @@ -207,11 +210,11 @@ public: using Layout = FossilizedValLayout; static const FossilizedValKind kKind = FossilizedValKind::Bool; - bool get() const { return _value != 0; } + SLANG_FORCE_INLINE bool get() const { return _value != 0; } - operator bool() const { return get(); } + SLANG_FORCE_INLINE operator bool() const { return get(); } - static bool isMatchingKind(FossilizedValKind kind) { return kind == kKind; } + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == kKind; } private: uint8_t _value; @@ -231,9 +234,9 @@ template<typename LiveType, typename FossilizedAsType> struct FossilizedViaCastVal { public: - LiveType get() const { return LiveType(_value.get()); } + SLANG_FORCE_INLINE LiveType get() const { return LiveType(_value.get()); } - operator LiveType() const { return get(); } + SLANG_FORCE_INLINE operator LiveType() const { return get(); } private: @@ -268,11 +271,11 @@ public: Size getSize() const; UnownedTerminatedStringSlice get() const; - operator UnownedTerminatedStringSlice() const { return get(); } + SLANG_FORCE_INLINE operator UnownedTerminatedStringSlice() const { return get(); } using Layout = FossilizedValLayout; - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == FossilizedValKind::StringObj; } @@ -308,9 +311,9 @@ public: Count getElementCount() const; - void const* getBuffer() const { return this; } + SLANG_FORCE_INLINE void const* getBuffer() const { return this; } - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { switch (kind) { @@ -342,7 +345,7 @@ public: struct FossilizedArrayObjBase : FossilizedContainerObjBase { public: - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == FossilizedValKind::ArrayObj; } @@ -419,13 +422,13 @@ struct FossilizedPtrLikeLayout struct FossilizedOptionalObjBase { public: - void* getValue() { return this; } + SLANG_FORCE_INLINE void* getValue() { return this; } - void const* getValue() const { return this; } + SLANG_FORCE_INLINE void const* getValue() const { return this; } using Layout = FossilizedPtrLikeLayout; - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == FossilizedValKind::OptionalObj; } @@ -440,17 +443,17 @@ private: template<typename T> struct FossilizedOptionalObj : FossilizedOptionalObjBase { - T* getValue() { return this; } + SLANG_FORCE_INLINE T* getValue() { return this; } - T const* getValue() const { return this; } + SLANG_FORCE_INLINE T const* getValue() const { return this; } }; template<typename T> struct FossilizedOptional { public: - explicit operator bool() const { return _value.get() != nullptr; } - T const& operator*() const { return *_value.get(); } + SLANG_FORCE_INLINE explicit operator bool() const { return _value.get() != nullptr; } + SLANG_FORCE_INLINE T const& operator*() const { return *_value.get(); } private: FossilizedPtr<T> _value; @@ -475,40 +478,40 @@ static_assert(sizeof(Fossilized<std::optional<double>>) == sizeof(FossilUInt)); struct FossilizedString { public: - Size getSize() const { return _obj ? _obj->getSize() : 0; } + SLANG_FORCE_INLINE Size getSize() const { return _obj ? _obj->getSize() : 0; } - UnownedTerminatedStringSlice get() const + SLANG_FORCE_INLINE UnownedTerminatedStringSlice get() const { return _obj ? _obj->get() : UnownedTerminatedStringSlice(); } - operator UnownedTerminatedStringSlice() const { return get(); } + SLANG_FORCE_INLINE operator UnownedTerminatedStringSlice() const { return get(); } private: FossilizedPtr<FossilizedStringObj> _obj; }; -inline int compare(FossilizedString const& lhs, UnownedStringSlice const& rhs) +SLANG_FORCE_INLINE int compare(FossilizedString const& lhs, UnownedStringSlice const& rhs) { return compare(lhs.get(), rhs); } -inline bool operator==(FossilizedString const& left, UnownedStringSlice const& right) +SLANG_FORCE_INLINE bool operator==(FossilizedString const& left, UnownedStringSlice const& right) { return left.get() == right; } -inline bool operator!=(FossilizedString const& left, UnownedStringSlice const& right) +SLANG_FORCE_INLINE bool operator!=(FossilizedString const& left, UnownedStringSlice const& right) { return left.get() != right; } -inline bool operator==(FossilizedStringObj const& left, UnownedStringSlice const& right) +SLANG_FORCE_INLINE bool operator==(FossilizedStringObj const& left, UnownedStringSlice const& right) { return left.get() == right; } -inline bool operator!=(FossilizedStringObj const& left, UnownedStringSlice const& right) +SLANG_FORCE_INLINE bool operator!=(FossilizedStringObj const& left, UnownedStringSlice const& right) { return left.get() != right; } @@ -531,21 +534,21 @@ template<typename T> struct FossilizedContainer { public: - Count getElementCount() const + SLANG_FORCE_INLINE Count getElementCount() const { if (!_obj) return 0; return _obj->getElementCount(); } - T const* getBuffer() const + SLANG_FORCE_INLINE T const* getBuffer() const { if (!_obj) return nullptr; return (T const*)_obj.get()->getBuffer(); } - T const* begin() const { return getBuffer(); } - T const* end() const { return getBuffer() + getElementCount(); } + SLANG_FORCE_INLINE T const* begin() const { return getBuffer(); } + SLANG_FORCE_INLINE T const* end() const { return getBuffer() + getElementCount(); } private: FossilizedPtr<FossilizedContainerObj<T>> _obj; @@ -555,7 +558,7 @@ template<typename T> struct FossilizedArray : FossilizedContainer<T> { public: - T const& operator[](Index index) const + SLANG_FORCE_INLINE T const& operator[](Index index) const { SLANG_ASSERT(index >= 0 && index < this->getElementCount()); return this->getBuffer()[index]; @@ -611,7 +614,7 @@ struct FossilizedTypeTraits<std::pair<K, V>> struct FossilizedDictionaryObjBase : FossilizedContainerObjBase { public: - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == FossilizedValKind::DictionaryObj; } @@ -677,7 +680,7 @@ struct FossilizedRecordVal public: using Layout = FossilizedRecordLayout; - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { switch (kind) { @@ -705,10 +708,10 @@ public: FossilizedValLayout* getContentLayout() const; - void* getContentDataPtr() { return this; } - void const* getContentDataPtr() const { return this; } + SLANG_FORCE_INLINE void* getContentDataPtr() { return this; } + SLANG_FORCE_INLINE void const* getContentDataPtr() const { return this; } - static bool isMatchingKind(FossilizedValKind kind) + SLANG_FORCE_INLINE static bool isMatchingKind(FossilizedValKind kind) { return kind == FossilizedValKind::VariantObj; } @@ -802,11 +805,11 @@ public: /// Construct a null reference. /// - ValRefBase() {} + SLANG_FORCE_INLINE ValRefBase() {} /// Construct a reference to the given `data`, assuming it has the given `layout`. /// - ValRefBase(T* data, Layout const* layout) + SLANG_FORCE_INLINE ValRefBase(T* data, Layout const* layout) : _data(data), _layout(layout) { } @@ -816,34 +819,36 @@ public: /// Only enabled if `U*` is convertible to `T*`. /// template<typename U> - ValRefBase(ValRefBase<U> ref, std::enable_if_t<std::is_convertible_v<U*, T*>, void>* = nullptr) + SLANG_FORCE_INLINE ValRefBase( + ValRefBase<U> ref, + std::enable_if_t<std::is_convertible_v<U*, T*>, void>* = nullptr) : _data(ref.getDataPtr()), _layout((Layout const*)ref.getLayout()) { } /// Get a pointer to the value being referenced. /// - T* getDataPtr() const { return _data; } + SLANG_FORCE_INLINE T* getDataPtr() const { return _data; } /// Get a reference to the value being referenced. /// /// This accessor is disabled in the case where `T` is `void`. /// template<typename U = T> - std::enable_if_t<!std::is_same_v<U, void>, T>& getDataRef() const + SLANG_FORCE_INLINE std::enable_if_t<!std::is_same_v<U, void>, T>& getDataRef() const { return *_data; } /// Get the layout of the value being referenced. /// - Layout const* getLayout() const { return _layout; } + SLANG_FORCE_INLINE Layout const* getLayout() const { return _layout; } /// Get the kind of value being referenced. /// /// This reference must not be null. /// - FossilizedValKind getKind() const + SLANG_FORCE_INLINE FossilizedValKind getKind() const { SLANG_ASSERT(getLayout()); return getLayout()->kind; @@ -881,12 +886,12 @@ public: /// Construct a null pointer. /// - ValPtr() {} - ValPtr(std::nullptr_t) {} + SLANG_FORCE_INLINE ValPtr() {} + SLANG_FORCE_INLINE ValPtr(std::nullptr_t) {} /// Construct a pointer to the given `data`, assuming it has the given `layout`. /// - ValPtr(T* data, TargetLayout const* layout) + SLANG_FORCE_INLINE ValPtr(T* data, TargetLayout const* layout) : _ref(data, layout) { } @@ -897,7 +902,7 @@ public: /// We define it as a constructor as a slightly more preferable alternative /// to overloading prefix `operator&` (which is almost always a Bad Idea) /// - explicit ValPtr(ValRef<T> ref) + SLANG_FORCE_INLINE explicit ValPtr(ValRef<T> ref) : _ref(ref) { } @@ -907,25 +912,27 @@ public: /// Only enabled if `U*` is convertible to `T*`. /// template<typename U> - ValPtr(ValPtr<U> ptr, std::enable_if_t<std::is_convertible_v<U*, T*>, void>* = nullptr) + SLANG_FORCE_INLINE ValPtr( + ValPtr<U> ptr, + std::enable_if_t<std::is_convertible_v<U*, T*>, void>* = nullptr) : _ref(*ptr) { } /// Get a pointer to the value being referenced. /// - T* getDataPtr() const { return _ref.getDataPtr(); } + SLANG_FORCE_INLINE T* getDataPtr() const { return _ref.getDataPtr(); } /// Get the layout of the value being referenced. /// - TargetLayout* getLayout() const { return _ref.getLayout(); } + SLANG_FORCE_INLINE TargetLayout const* getLayout() const { return _ref.getLayout(); } - T* get() const { return _ref.getDataPtr(); } - operator T*() const { return get(); } + SLANG_FORCE_INLINE T* get() const { return _ref.getDataPtr(); } + SLANG_FORCE_INLINE operator T*() const { return get(); } /// Deference this `ValPtr` to get a `ValRef`. /// - ValRef<T> operator*() const { return _ref; } + SLANG_FORCE_INLINE ValRef<T> operator*() const { return _ref; } /// Deference this `ValPtr` for member access. /// @@ -939,7 +946,7 @@ public: /// that behavior is for the `operator->` on `ValPtr` /// to return a pointer to a `ValRef`. /// - ValRef<T> const* operator->() const { return &_ref; } + SLANG_FORCE_INLINE ValRef<T> const* operator->() const { return &_ref; } private: ValRef<T> _ref; @@ -948,7 +955,7 @@ private: /// Get a `ValPtr` pointing to the same value as the given `ref`. /// template<typename T> -inline ValPtr<T> getAddress(ValRef<T> ref) +SLANG_FORCE_INLINE ValPtr<T> getAddress(ValRef<T> ref) { return ValPtr<T>(ref); } @@ -973,10 +980,10 @@ struct ValRef<FossilizedStringObj> : ValRefBase<FossilizedStringObj> public: using ValRefBase<FossilizedStringObj>::ValRefBase; - Size getSize() const { return getDataPtr()->getSize(); } - UnownedTerminatedStringSlice get() const { return getDataPtr()->get(); } + SLANG_FORCE_INLINE Size getSize() const { return getDataPtr()->getSize(); } + SLANG_FORCE_INLINE UnownedTerminatedStringSlice get() const { return getDataPtr()->get(); } - operator UnownedTerminatedStringSlice() const { return get(); } + SLANG_FORCE_INLINE operator UnownedTerminatedStringSlice() const { return get(); } }; @@ -986,7 +993,7 @@ struct ValRef<FossilizedContainerObjBase> : ValRefBase<FossilizedContainerObjBas public: using ValRefBase<FossilizedContainerObjBase>::ValRefBase; - Count getElementCount() const + SLANG_FORCE_INLINE Count getElementCount() const { auto data = this->getDataPtr(); if (!data) @@ -1004,7 +1011,7 @@ struct ValRef<FossilizedArrayObjBase> : ValRefBase<FossilizedArrayObjBase> public: using ValRefBase<FossilizedArrayObjBase>::ValRefBase; - Count getElementCount() const + SLANG_FORCE_INLINE Count getElementCount() const { auto data = this->getDataPtr(); if (!data) @@ -1022,7 +1029,7 @@ struct ValRef<FossilizedDictionaryObjBase> : ValRefBase<FossilizedDictionaryObjB public: using ValRefBase<FossilizedDictionaryObjBase>::ValRefBase; - Count getElementCount() const + SLANG_FORCE_INLINE Count getElementCount() const { auto data = this->getDataPtr(); if (!data) @@ -1038,9 +1045,9 @@ struct ValRef<FossilizedOptionalObjBase> : ValRefBase<FossilizedOptionalObjBase> public: using ValRefBase<FossilizedOptionalObjBase>::ValRefBase; - bool hasValue() const { return this->getDataPtr() != nullptr; } + SLANG_FORCE_INLINE bool hasValue() const { return this->getDataPtr() != nullptr; } - AnyValRef getValue() const + SLANG_FORCE_INLINE AnyValRef getValue() const { SLANG_ASSERT(hasValue()); return AnyValRef(this->getDataPtr(), this->getLayout()->elementLayout.get()); @@ -1053,7 +1060,7 @@ struct ValRef<FossilizedRecordVal> : ValRefBase<FossilizedRecordVal> public: using ValRefBase<FossilizedRecordVal>::ValRefBase; - Count getFieldCount() const { return getLayout()->fieldCount; } + SLANG_FORCE_INLINE Count getFieldCount() const { return getLayout()->fieldCount; } AnyValRef getField(Index index) const; }; @@ -1064,13 +1071,13 @@ struct ValRef<FossilizedPtr<T>> : ValRefBase<FossilizedPtr<T>> public: using ValRefBase<FossilizedPtr<T>>::ValRefBase; - ValRef<T> getTargetValRef() const + SLANG_FORCE_INLINE ValRef<T> getTargetValRef() const { auto ptrPtr = this->getDataPtr(); return ValRef<T>(*ptrPtr, this->getLayout()->elementLayout.get()); } - ValPtr<T> getTargetValPtr() const { return ValPtr<T>(getTargetValRef()); } + SLANG_FORCE_INLINE ValPtr<T> getTargetValPtr() const { return ValPtr<T>(getTargetValRef()); } // ValRef<T> operator*() const; }; @@ -1085,19 +1092,19 @@ public: /// Statically cast a pointer to a fossilized value. /// template<typename T> -ValPtr<T> cast(AnyValPtr valPtr) +SLANG_FORCE_INLINE ValPtr<T> cast(AnyValPtr valPtr) { - if (!valPtr) - return ValPtr<T>(); + // if (!valPtr) + // return ValPtr<T>(); return ValPtr<T>( static_cast<T*>(valPtr.getDataPtr()), - (typename T::Layout*)(valPtr->getLayout())); + (typename T::Layout*)(valPtr.getLayout())); } /// Dynamic cast of a pointer to a fossilized value. /// template<typename T> -ValPtr<T> as(AnyValPtr valPtr) +SLANG_FORCE_INLINE ValPtr<T> as(AnyValPtr valPtr) { if (!valPtr || !T::isMatchingKind(valPtr->getKind())) { @@ -1174,6 +1181,36 @@ Fossil::AnyValPtr getRootValue(ISlangBlob* blob); Fossil::AnyValPtr getRootValue(void const* data, Size size); } // namespace Fossil +SLANG_FORCE_INLINE Size FossilizedStringObj::getSize() const +{ + auto sizePtr = (FossilUInt*)this - 1; + return Size(*sizePtr); +} + +SLANG_FORCE_INLINE UnownedTerminatedStringSlice FossilizedStringObj::get() const +{ + auto size = getSize(); + return UnownedTerminatedStringSlice((char*)this, size); +} + +SLANG_FORCE_INLINE Count FossilizedContainerObjBase::getElementCount() const +{ + auto countPtr = (FossilUInt*)this - 1; + return Size(*countPtr); +} + +SLANG_FORCE_INLINE FossilizedValLayout* FossilizedVariantObj::getContentLayout() const +{ + auto layoutPtrPtr = (FossilizedPtr<FossilizedValLayout>*)this - 1; + return (*layoutPtrPtr).get(); +} + +SLANG_FORCE_INLINE Fossil::AnyValPtr getVariantContentPtr(FossilizedVariantObj* variantPtr) +{ + return Fossil::AnyValPtr(variantPtr->getContentDataPtr(), variantPtr->getContentLayout()); +} + + } // namespace Slang #endif diff --git a/source/slang/slang-serialize-ast.cpp b/source/slang/slang-serialize-ast.cpp index a288b3bd2..be959b8a1 100644 --- a/source/slang/slang-serialize-ast.cpp +++ b/source/slang/slang-serialize-ast.cpp @@ -1,6 +1,7 @@ // slang-serialize-ast.cpp #include "slang-serialize-ast.h" +#include "core/slang-performance-profiler.h" #include "slang-ast-dispatch.h" #include "slang-check.h" #include "slang-compiler.h" @@ -84,7 +85,8 @@ namespace Slang // they are all just getting dumped here in the AST serialization logic, because // it is currenly the only place that cares about this stuff. // -void serialize(Serializer const&, RefObject&) +template<typename S> +void serialize(S const&, RefObject&) { // There's actually no data stored in a `RefObject`, since it only exists // to make reference-counting possible for other types. This function is @@ -136,7 +138,8 @@ struct FossilizedTypeTraits<RefObject> // While we could include this among the types we handle using fiddle, // let's implement it by hand here, starting with the `serialize()` function: // -void serialize(Serializer const& serializer, MatrixCoord& value) +template<typename S> +void serialize(S const& serializer, MatrixCoord& value) { // We start with one of the `SLANG_SCOPED_SERIALIZER_*` // macros, which basically just handles calling @@ -210,7 +213,8 @@ struct FossilizedTypeTraits<MatrixCoord> // as a single scalar value. We'll define our `serialize()` function // so that it serializes that "raw" value instead: // -void serialize(Serializer const& serializer, SemanticVersion& value) +template<typename S> +void serialize(S const& serializer, SemanticVersion& value) { // This function is doing something a little "clever" // handle the fact that it might be used to either @@ -373,33 +377,17 @@ struct ContainerDeclDirectMemberDeclsInfo }; // -// Okay, that's enough examples for now. Let's move on to the next big -// topic... -// // Many types in the AST need additional context information to be able to -// read or write them properly, so instead of passing around the basic -// `Serializer` type (which wraps an `ISerializerImpl`), for those types -// that need extra context we will be passing around an `ASTSerializer` -// (which wraps an `IASTSerializerImpl`, with the latter interface providing -// the callbacks to handle the data types that need special-case behavior. +// read or write them properly, so the concrete serializer type being passed +// around will include an additional "context" type, that will be either +// `ASTSerialReadContext` or `ASTSerialWriteContext`, depending on the mode +// in which serialization is being performed. +// +// We could define a base class or interface with `virtual` functions for +// accessing all of the relevant context, but because we are already +// using template specialization, it is easier to just ensure that the +// relevant context types both provide the required operations. // - -struct ASTSerialContext; -using ASTSerializer = Serializer_<ISerializerImpl, ASTSerialContext>; - -/// Context interface for AST serialization -struct ASTSerialContext : SourceLocSerialContext -{ -public: - virtual void handleASTNode(ASTSerializer const& serializer, NodeBase*& value) = 0; - virtual void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* value) = 0; - virtual void handleName(ASTSerializer const& serializer, Name*& value) = 0; - virtual void handleToken(ASTSerializer const& serializer, Token& value) = 0; - virtual void handleContainerDeclDirectMemberDecls( - ASTSerializer const& serializer, - ContainerDeclDirectMemberDecls& value) = 0; -}; - // // Now that we've covered some of the big-picture structure, and shown @@ -439,7 +427,8 @@ public: %for _,T in ipairs(enumTypeNames) do /// Serialize a `value` of type `$T`. -void serialize(Serializer const& serializer, $T& value) +template<typename S> +void serialize(S const& serializer, $T& value) { serializeEnum(serializer, value); } @@ -518,7 +507,8 @@ struct Fossilized_$T; SLANG_DECLARE_FOSSILIZED_TYPE($T, Fossilized_$T); /// Serialize a `$T` -void serialize(ASTSerializer const& serializer, $T& value); +template<typename S> +void serialize(S const& serializer, $T& value); %end #else // FIDDLE OUTPUT: #define FIDDLE_GENERATED_OUTPUT_ID 1 @@ -545,7 +535,8 @@ struct Fossilized_$T; SLANG_DECLARE_FOSSILIZED_TYPE($T, Fossilized_$T); /// Serialize the content of a `$T` -void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value); +template<typename S> +void _serializeASTNodeContents(S const& serializer, $T* value); %end #else // FIDDLE OUTPUT: #define FIDDLE_GENERATED_OUTPUT_ID 2 @@ -566,9 +557,11 @@ void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value); /// lower-level serialization operations to an underlying /// `ISerializerImpl`. /// -struct ASTSerialWriteContext : ASTSerialContext +struct ASTSerialWriteContext : SourceLocSerialContext { public: + using ASTSerializer = Serializer<Fossil::SerialWriter, ASTSerialWriteContext>; + /// Construct a context for writing a serialized AST. /// /// * `module` is the module that is being serialized, and will be @@ -587,21 +580,23 @@ private: ModuleDecl* _module = nullptr; SerialSourceLocWriter* _sourceLocWriter = nullptr; +public: // // For the most part, this type just implements the methods // of the `IASTSerializerImpl` interface, and then has some // support routines needed by those implementations. // - virtual void handleName(ASTSerializer const& serializer, Name*& value) override; - virtual void handleToken(ASTSerializer const& serializer, Token& value) override; - virtual void handleASTNode(ASTSerializer const& serializer, NodeBase*& node) override; - virtual void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node) override; - virtual void handleContainerDeclDirectMemberDecls( + void handleName(ASTSerializer const& serializer, Name*& value); + void handleToken(ASTSerializer const& serializer, Token& value); + void handleASTNode(ASTSerializer const& serializer, NodeBase*& node); + void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node); + void handleContainerDeclDirectMemberDecls( ASTSerializer const& serializer, - ContainerDeclDirectMemberDecls& value) override; - virtual SerialSourceLocWriter* getSourceLocWriter() override { return _sourceLocWriter; } + ContainerDeclDirectMemberDecls& value); + SerialSourceLocWriter* getSourceLocWriter() { return _sourceLocWriter; } +private: void _writeImportedModule(ASTSerializer const& serializer, ModuleDecl* moduleDecl); void _writeImportedDecl( ASTSerializer const& serializer, @@ -653,9 +648,11 @@ private: /// contexts could result in the same declaration getting turned /// into multiple distinct `Decl*`s. /// -struct ASTSerialReadContext : public ASTSerialContext, public RefObject +struct ASTSerialReadContext : public SourceLocSerialContext, public RefObject { public: + using ASTSerializer = Serializer<Fossil::SerialReader, ASTSerialReadContext>; + /// Construct an AST deserialization context. /// /// The `linkage`, `astBuilder`, and `sink` arguments must @@ -729,6 +726,7 @@ private: Count _deserializedTopLevelDeclCount = 0; #endif +public: // // Much like the `ASTSerialWriter`, for the most part this // type just implements the `IASTSerializer` interface, @@ -736,15 +734,16 @@ private: // implementations. // - virtual void handleName(ASTSerializer const& serializer, Name*& value) override; - virtual void handleToken(ASTSerializer const& serializer, Token& value) override; - virtual void handleASTNode(ASTSerializer const& serializer, NodeBase*& outNode) override; - virtual void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node) override; - virtual void handleContainerDeclDirectMemberDecls( + void handleName(ASTSerializer const& serializer, Name*& value); + void handleToken(ASTSerializer const& serializer, Token& value); + void handleASTNode(ASTSerializer const& serializer, NodeBase*& outNode); + void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node); + void handleContainerDeclDirectMemberDecls( ASTSerializer const& serializer, - ContainerDeclDirectMemberDecls& value) override; - virtual SerialSourceLocReader* getSourceLocReader() override { return _sourceLocReader; } + ContainerDeclDirectMemberDecls& value); + SerialSourceLocReader* getSourceLocReader() { return _sourceLocReader; } +private: ModuleDecl* _readImportedModule(ASTSerializer const& serializer); NodeBase* _readImportedDecl(ASTSerializer const& serializer); @@ -770,7 +769,8 @@ private: SLANG_DECLARE_FOSSILIZED_AS(Name, String); -void serializeObject(ASTSerializer const& serializer, Name*& value, Name*) +template<typename S> +void serializeObject(S const& serializer, Name*& value, Name*) { serializer.getContext()->handleName(serializer, value); } @@ -807,7 +807,8 @@ struct FossilizedTypeTraits<Token> }; }; -void serialize(ASTSerializer const& serializer, Token& value) +template<typename S> +void serialize(S const& serializer, Token& value) { serializer.getContext()->handleToken(serializer, value); } @@ -883,8 +884,8 @@ void ASTSerialReadContext::handleToken(ASTSerializer const& serializer, Token& v // serialize any pointers to AST nodes. // -template<typename T> -void serializeObject(ASTSerializer const& serializer, T*& value, NodeBase*) +template<typename S, typename T> +SLANG_FORCE_INLINE void serializeObject(S const& serializer, T*& value, NodeBase*) { // The general-purpose serialization layer defines // a variant as akin to a struct, but where the @@ -912,7 +913,8 @@ void serializeObject(ASTSerializer const& serializer, T*& value, NodeBase*) // object in the reading direction. // -void serializeObjectContents(ASTSerializer const& serializer, NodeBase* value, NodeBase*) +template<typename S> +SLANG_FORCE_INLINE void serializeObjectContents(S const& serializer, NodeBase* value, NodeBase*) { serializer.getContext()->handleASTNodeContents(serializer, value); } @@ -930,7 +932,8 @@ void serializeObjectContents(ASTSerializer const& serializer, NodeBase* value, N SLANG_DECLARE_FOSSILIZED_AS(ContainerDeclDirectMemberDecls, ContainerDeclDirectMemberDeclsInfo); -void serialize(ASTSerializer const& serializer, ContainerDeclDirectMemberDecls& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, ContainerDeclDirectMemberDecls& value) { serializer.getContext()->handleContainerDeclDirectMemberDecls(serializer, value); } @@ -943,7 +946,8 @@ void serialize(ASTSerializer const& serializer, ContainerDeclDirectMemberDecls& SLANG_DECLARE_FOSSILIZED_AS(DiagnosticInfo const*, Int32); -void serializePtr(Serializer const& serializer, DiagnosticInfo const*& value, DiagnosticInfo const*) +template<typename S> +void serializePtr(S const& serializer, DiagnosticInfo const*& value, DiagnosticInfo const*) { Int32 id = 0; if (isWriting(serializer)) @@ -964,8 +968,8 @@ void serializePtr(Serializer const& serializer, DiagnosticInfo const*& value, Di // and we'll serialize it as such. // -template<typename T> -void serialize(ASTSerializer const& serializer, DeclRef<T>& value) +template<typename S, typename T> +void serialize(S const& serializer, DeclRef<T>& value) { serialize(serializer, value.declRefBase); } @@ -987,7 +991,8 @@ struct FossilizedTypeTraits<DeclRef<T>> SLANG_DECLARE_FOSSILIZED_AS(SyntaxClass<NodeBase>, ASTNodeType); -void serialize(Serializer const& serializer, SyntaxClass<NodeBase>& value) +template<typename S> +void serialize(S const& serializer, SyntaxClass<NodeBase>& value) { ASTNodeType raw = ASTNodeType(0); if (isWriting(serializer)) @@ -1011,7 +1016,8 @@ void serialize(Serializer const& serializer, SyntaxClass<NodeBase>& value) SLANG_DECLARE_FOSSILIZED_AS(Modifiers, List<Modifier*>); -void serialize(ASTSerializer const& serializer, Modifiers& value) +template<typename S> +void serialize(S const& serializer, Modifiers& value) { SLANG_SCOPED_SERIALIZER_ARRAY(serializer); @@ -1059,7 +1065,8 @@ void serialize(ASTSerializer const& serializer, Modifiers& value) // SLANG_DECLARE_FOSSILIZED_AS_MEMBER(TypeExp, type); -void serialize(ASTSerializer const& serializer, TypeExp& value) +template<typename S> +void serialize(S const& serializer, TypeExp& value) { serialize(serializer, value.type); } @@ -1071,7 +1078,8 @@ void serialize(ASTSerializer const& serializer, TypeExp& value) SLANG_DECLARE_FOSSILIZED_AS_MEMBER(CandidateExtensionList, candidateExtensions); -void serialize(ASTSerializer const& serializer, CandidateExtensionList& value) +template<typename S> +void serialize(S const& serializer, CandidateExtensionList& value) { serialize(serializer, value.candidateExtensions); } @@ -1079,7 +1087,8 @@ void serialize(ASTSerializer const& serializer, CandidateExtensionList& value) SLANG_DECLARE_FOSSILIZED_AS_MEMBER(DeclAssociationList, associations); -void serialize(ASTSerializer const& serializer, DeclAssociationList& value) +template<typename S> +void serialize(S const& serializer, DeclAssociationList& value) { serialize(serializer, value.associations); } @@ -1109,7 +1118,8 @@ SLANG_DECLARE_FOSSILIZED_AS(CapabilityTargetSet, CapabilityStageSets); // SLANG_DECLARE_FOSSILIZED_AS(CapabilitySet, CapabilityTargetSets); -void serialize(Serializer const& serializer, CapabilityAtomSet& value) +template<typename S> +void serialize(S const& serializer, CapabilityAtomSet& value) { SLANG_SCOPED_SERIALIZER_ARRAY(serializer); if (isWriting(serializer)) @@ -1131,12 +1141,14 @@ void serialize(Serializer const& serializer, CapabilityAtomSet& value) } } -void serialize(Serializer const& serializer, CapabilityStageSet& value) +template<typename S> +void serialize(S const& serializer, CapabilityStageSet& value) { serialize(serializer, value.atomSet); } -void serialize(Serializer const& serializer, CapabilityTargetSet& value) +template<typename S> +void serialize(S const& serializer, CapabilityTargetSet& value) { serialize(serializer, value.shaderStageSets); @@ -1154,7 +1166,8 @@ void serialize(Serializer const& serializer, CapabilityTargetSet& value) } } -void serialize(Serializer const& serializer, CapabilitySet& value) +template<typename S> +void serialize(S const& serializer, CapabilitySet& value) { serialize(serializer, value.getCapabilityTargetSets()); @@ -1190,7 +1203,8 @@ struct FossilizedTypeTraits<RequirementWitness> }; }; -void serialize(ASTSerializer const& serializer, RequirementWitness& value) +template<typename S> +void serialize(S const& serializer, RequirementWitness& value) { SLANG_SCOPED_SERIALIZER_VARIANT(serializer); serialize(serializer, value.m_flavor); @@ -1228,7 +1242,8 @@ struct FossilizedTypeTraits<ValNodeOperand> }; }; -void serialize(ASTSerializer const& serializer, ValNodeOperand& value) +template<typename S> +void serialize(S const& serializer, ValNodeOperand& value) { SLANG_SCOPED_SERIALIZER_VARIANT(serializer); serialize(serializer, value.kind); @@ -1289,7 +1304,8 @@ struct Fossilized_$T }; /// Serialize a `value` of type `$T` -void serialize(ASTSerializer const& serializer, $T& value) +template<typename S> +void serialize(S const& serializer, $T& value) { SLANG_UNUSED(value); SLANG_SCOPED_SERIALIZER_STRUCT(serializer); @@ -1346,7 +1362,8 @@ struct Fossilized_$T }; /// Serialize the contents of an AST node of type `$T` -void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value) +template<typename S> +void _serializeASTNodeContents(S const& serializer, $T* value) { SLANG_UNUSED(serializer); SLANG_UNUSED(value); @@ -1372,7 +1389,8 @@ void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value) // functions, and dispatches to the correct one based on the type of the given node. // -void serializeASTNodeContents(ASTSerializer const& serializer, NodeBase* node) +template<typename S> +void serializeASTNodeContents(S const& serializer, NodeBase* node) { ASTNodeDispatcher<NodeBase, void>::dispatch( node, @@ -1894,7 +1912,7 @@ void ASTSerialReadContext::handleContainerDeclDirectMemberDecls( // `ISerializerImpl`, whereas we *know* it has a more // specific type, which we want to make use of. // - ISerializerImpl* readerImpl = serializer.getImpl(); + auto readerImpl = serializer.getImpl(); auto fossilReader = static_cast<Fossil::SerialReader*>(readerImpl); // auto fossilizedInfo = @@ -1927,6 +1945,8 @@ void writeSerializedModuleAST( ModuleDecl* moduleDecl, SerialSourceLocWriter* sourceLocWriter) { + SLANG_PROFILE; + // TODO: we might want to have a more careful pass here, // where we only encode the public declarations. @@ -1965,7 +1985,7 @@ void writeSerializedModuleAST( // Fossil::SerialWriter writer(blobBuilder); ASTSerialWriteContext context(moduleDecl, sourceLocWriter); - ASTSerializer serializer(&writer, &context); + ASTSerialWriteContext::ASTSerializer serializer(&writer, &context); // Once we have our `serializer`, we can finally invoke // `serialize()` on the `ASTModuleInfo` to cause everything @@ -2030,6 +2050,8 @@ ModuleDecl* readSerializedModuleAST( SerialSourceLocReader* sourceLocReader, SourceLoc requestingSourceLoc) { + SLANG_PROFILE; + // We expect the `chunk` that was passed in to be a RIFF // data chunk (matching what was written in `writeSerializedModuleAST()`, // and to be proper fossil-format data. diff --git a/source/slang/slang-serialize-fossil.cpp b/source/slang/slang-serialize-fossil.cpp index 7ce09d283..3a3d8c00f 100644 --- a/source/slang/slang-serialize-fossil.cpp +++ b/source/slang/slang-serialize-fossil.cpp @@ -2,6 +2,7 @@ #include "slang-serialize-fossil.h" #include "../core/slang-blob.h" +#include "core/slang-performance-profiler.h" namespace Slang { @@ -142,16 +143,17 @@ void SerialWriter::handleString(String& value) auto size = value.getLength(); if (_shouldEmitPotentiallyIndirectValueWithPointerIndirection()) { - if (size == 0) - { - _writeNull(); - return; - } - - if (auto found = _mapStringToChunk.tryGetValue(value)) + ChunkBuilder* existingChunk = nullptr; + _mapStringToChunk.tryGetValue(value, existingChunk); + + // If we found an existing chunk that holds the string + // value in question, we can re-use it. Also, if the + // string is empty, we can encode it as a null pointer + // in this case, so we don't care if we found a chunk + // or not. + // + if (existingChunk || size == 0) { - auto existingChunk = *found; - auto ptrLayout = (ContainerLayoutObj*)_reserveDestinationForWrite(FossilizedValKind::Ptr); _mergeLayout(ptrLayout->baseLayout, FossilizedValKind::StringObj); @@ -174,22 +176,22 @@ void SerialWriter::handleString(String& value) _mapStringToChunk.addIfNotExists(value, chunk); } -void SerialWriter::beginArray() +void SerialWriter::beginArray(Scope&) { _pushContainerScope(FossilizedValKind::ArrayObj); } -void SerialWriter::endArray() +void SerialWriter::endArray(Scope&) { _popContainerScope(); } -void SerialWriter::beginDictionary() +void SerialWriter::beginDictionary(Scope&) { _pushContainerScope(FossilizedValKind::DictionaryObj); } -void SerialWriter::endDictionary() +void SerialWriter::endDictionary(Scope&) { _popContainerScope(); } @@ -216,23 +218,23 @@ bool SerialWriter::hasElements() return false; } -void SerialWriter::beginStruct() +void SerialWriter::beginStruct(Scope&) { _pushInlineValueScope(FossilizedValKind::Struct); } -void SerialWriter::endStruct() +void SerialWriter::endStruct(Scope&) { _popInlineValueScope(); } -void SerialWriter::beginVariant() +void SerialWriter::beginVariant(Scope&) { _pushVariantScope(); _pushInlineValueScope(FossilizedValKind::Struct); } -void SerialWriter::endVariant() +void SerialWriter::endVariant(Scope&) { _popInlineValueScope(); _popVariantScope(); @@ -246,27 +248,27 @@ void SerialWriter::handleFieldKey(char const* name, Int index) SLANG_UNUSED(index); } -void SerialWriter::beginTuple() +void SerialWriter::beginTuple(Scope&) { _pushInlineValueScope(FossilizedValKind::Tuple); } -void SerialWriter::endTuple() +void SerialWriter::endTuple(Scope&) { _popInlineValueScope(); } -void SerialWriter::beginOptional() +void SerialWriter::beginOptional(Scope&) { _pushIndirectValueScope(FossilizedValKind::OptionalObj); } -void SerialWriter::endOptional() +void SerialWriter::endOptional(Scope&) { _popIndirectValueScope(); } -void SerialWriter::handleSharedPtr(void*& value, Callback callback, void* context) +void SerialWriter::handleSharedPtr(void*& value, SerializerCallback callback, void* context) { // Because we are writing, we only care about the // pointer that is already present in `value`. @@ -313,7 +315,7 @@ void SerialWriter::handleSharedPtr(void*& value, Callback callback, void* contex _commitWrite(ValInfo::relativePtrTo(chunk)); } -void SerialWriter::handleUniquePtr(void*& value, Callback callback, void* context) +void SerialWriter::handleUniquePtr(void*& value, SerializerCallback callback, void* context) { // We treat all pointers as shared pointers, because there isn't really // an optimized representation we would want to use for the unique case. @@ -321,7 +323,10 @@ void SerialWriter::handleUniquePtr(void*& value, Callback callback, void* contex handleSharedPtr(value, callback, context); } -void SerialWriter::handleDeferredObjectContents(void* valuePtr, Callback callback, void* context) +void SerialWriter::handleDeferredObjectContents( + void* valuePtr, + SerializerCallback callback, + void* context) { // Because we are already deferring writing of the *entirety* of // an object's members as part of how `handleSharedPtr()` works, @@ -1152,7 +1157,7 @@ SerialReader::SerialReader( switch (initialState) { case InitialStateType::Root: - _state.type = State::Type::Root; + _state.type = State::Type::Object; break; case InitialStateType::PseudoPtr: @@ -1160,9 +1165,9 @@ SerialReader::SerialReader( break; } - _state.baseValPtr = valPtr; - _state.elementIndex = 0; - _state.elementCount = 1; + _state.dataCursor = valPtr.getDataPtr(); + _state.layoutCursor = valPtr.getLayout(); + _state.remainingValueCount = 1; } SerialReader::~SerialReader() @@ -1207,149 +1212,12 @@ SerialReader::~SerialReader() _context._readerCount--; } -Fossil::AnyValPtr SerialReader::readValPtr() -{ - return _readValPtr(); -} - void SerialReader::flush() { _flush(); } -SerializationMode SerialReader::getMode() -{ - return SerializationMode::Read; -} - -void SerialReader::handleBool(bool& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleInt8(int8_t& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleInt16(int16_t& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleInt32(Int32& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleInt64(Int64& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleUInt8(uint8_t& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleUInt16(uint16_t& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleUInt32(UInt32& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleUInt64(UInt64& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleFloat32(float& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleFloat64(double& value) -{ - handleSimpleVal(value); -} - -void SerialReader::handleString(String& value) -{ - auto valPtr = _readPotentiallyIndirectValPtr(); - if (!valPtr) - { - value = String(); - } - else - { - value = as<FossilizedStringObj>(valPtr)->get(); - } -} - -void SerialReader::beginArray() -{ - auto valPtr = _readPotentiallyIndirectValPtr(); - auto arrayPtr = as<FossilizedArrayObjBase>(valPtr); - - _pushState(); - - _state.type = State::Type::Array; - _state.baseValPtr = arrayPtr; - _state.elementIndex = 0; - _state.elementCount = arrayPtr->getElementCount(); -} - -void SerialReader::endArray() -{ - _popState(); -} - -void SerialReader::beginDictionary() -{ - auto valPtr = _readPotentiallyIndirectValPtr(); - auto dictionaryPtr = as<FossilizedDictionaryObjBase>(valPtr); - - _pushState(); - - _state.type = State::Type::Dictionary; - _state.baseValPtr = dictionaryPtr; - _state.elementIndex = 0; - _state.elementCount = dictionaryPtr->getElementCount(); -} - -void SerialReader::endDictionary() -{ - _popState(); -} - -bool SerialReader::hasElements() -{ - return _state.elementIndex < _state.elementCount; -} - -void SerialReader::beginStruct() -{ - auto valPtr = _readValPtr(); - auto recordPtr = as<FossilizedRecordVal>(valPtr); - - _pushState(); - - _state.type = State::Type::Struct; - _state.baseValPtr = valPtr; - _state.elementIndex = 0; - _state.elementCount = recordPtr->getFieldCount(); -} - -void SerialReader::endStruct() -{ - _popState(); -} - -void SerialReader::beginVariant() +void SerialReader::beginVariant(Scope& scope) { auto valPtr = _readPotentiallyIndirectValPtr(); if (auto variantPtr = as<FossilizedVariantObj>(valPtr)) @@ -1357,78 +1225,28 @@ void SerialReader::beginVariant() auto contentValPtr = getVariantContentPtr(variantPtr); valPtr = contentValPtr; } - auto recordPtr = as<FossilizedRecordVal>(valPtr); - - _pushState(); - - _state.type = State::Type::Struct; - _state.baseValPtr = recordPtr; - _state.elementIndex = 0; - _state.elementCount = recordPtr->getFieldCount(); -} - -void SerialReader::endVariant() -{ - _popState(); -} - -void SerialReader::handleFieldKey(char const* name, Int index) -{ - // For now we are ignoring field keys, and treating - // structs as basically equivalent to tuples. - SLANG_UNUSED(name); - SLANG_UNUSED(index); -} - -void SerialReader::beginTuple() -{ - auto valPtr = _readValPtr(); - auto recordPtr = as<FossilizedRecordVal>(valPtr); - - _pushState(); - - _state.type = State::Type::Tuple; - _state.baseValPtr = recordPtr; - _state.elementIndex = 0; - _state.elementCount = recordPtr->getFieldCount(); -} - -void SerialReader::endTuple() -{ - _popState(); -} - -void SerialReader::beginOptional() -{ - auto valPtr = _readIndirectValPtr(); - auto optionalPtr = as<FossilizedOptionalObjBase>(valPtr); - - _pushState(); + auto recordPtr = expectNonNullValOfType<FossilizedRecordVal>(valPtr); - _state.type = State::Type::Optional; - _state.baseValPtr = optionalPtr; - _state.elementIndex = 0; - _state.elementCount = Count(optionalPtr->hasValue()); + _pushRecordState(scope, recordPtr); } -void SerialReader::endOptional() +void SerialReader::handleSharedPtr(void*& value, SerializerCallback callback, void* context) { - _popState(); -} + Fossil::AnyValPtr valPtr = _readValPtr(); -void SerialReader::handleSharedPtr(void*& value, Callback callback, void* context) -{ Fossil::AnyValPtr targetValPtr; - if (_state.type == State::Type::PseudoPtr) { - _state.type = State::Type::Root; - targetValPtr = _readValPtr(); + // TODO(tfoley): Having to include the `PseudoPtr` case here + // is frustrating, because it was only introduced to deal + // with a wrinkle related to on-demand AST deserialization, + // and ideally shouldn't be needed at all. + + targetValPtr = valPtr; } else { - auto valPtr = _readValPtr(); - auto ptrPtr = as<FossilizedPtr<void>>(valPtr); + auto ptrPtr = expectNonNullValOfType<FossilizedPtr<void>>(valPtr); targetValPtr = ptrPtr->getTargetValPtr(); } @@ -1521,11 +1339,12 @@ void SerialReader::handleSharedPtr(void*& value, Callback callback, void* contex // reading whatever comes after the pointer // we were invoked to read. // - _pushState(); + Scope callbackScope; + _pushState(callbackScope); _state.type = State::Type::Object; - _state.baseValPtr = objectInfo->fossilizedObjectPtr; - _state.elementIndex = 0; - _state.elementCount = 1; + _state.dataCursor = targetValPtr.getDataPtr(); + _state.layoutCursor = targetValPtr.getLayout(); + _state.remainingValueCount = 1; // Note that we are passing the address of `objectInfo.ptr`, // and `objectInfo` is a reference to an element of the @@ -1543,22 +1362,17 @@ void SerialReader::handleSharedPtr(void*& value, Callback callback, void* contex // callback(&objectInfo->resurrectedObjectPtr, this, context); - _popState(); + _popState(callbackScope); objectInfo->state = ObjectState::ReadingComplete; value = objectInfo->resurrectedObjectPtr; } -void SerialReader::handleUniquePtr(void*& value, Callback callback, void* context) -{ - // We treat all pointers as shared pointers, because there isn't really - // an optimized representation we would want to use for the unique case. - // - handleSharedPtr(value, callback, context); -} - -void SerialReader::handleDeferredObjectContents(void* valuePtr, Callback callback, void* context) +void SerialReader::handleDeferredObjectContents( + void* valuePtr, + SerializerCallback callback, + void* context) { // Unlike the case in `SerialWriter::handleDeferredObjectContents()`, // we very much *do* want to delay invoking the callback until later. @@ -1604,57 +1418,100 @@ void SerialReader::_flush() } } -Fossil::AnyValPtr SerialReader::_readValPtr() +void SerialReader::_advanceCursor() { - switch (_state.type) + auto dataPtr = _state.dataCursor; + + // The state also tracks the number of values that + // can still be read in the current scope. This value + // is used both to drive the loop when an application + // is reading a collection, and as a validation check + // here. + // + auto remainingValueCount = _state.remainingValueCount; + SLANG_SERIALIZE_FOSSIL_VALIDATE(remainingValueCount > 0); + + // At minimum, we need to update the state to reflect + // the new number of values that remain. + // + remainingValueCount--; + _state.remainingValueCount = remainingValueCount; + + // At this point, there is no need to update the + // state further, unless values remain. While + // we expect the "no values remain" case to be + // the less common one, we still guard all of + // this logic because we already need to branch + // to handle the case of record fields. + // + if (remainingValueCount != 0) { - case State::Type::Root: - case State::Type::Object: - case State::Type::PseudoPtr: - SLANG_ASSERT(_state.elementCount == 1); - SLANG_ASSERT(_state.elementIndex == 0); - _state.elementIndex++; - return _state.baseValPtr; - - case State::Type::Struct: - case State::Type::Tuple: + // The primary task in this case is to update + // the data pointer to point to the next value + // in the stream. + // + auto nextDataPtr = (char*)dataPtr; + + // If we are reading from a record (struct or tuple) + // then we will have an additional piece of state + // indicating the field that the data cursor was + // pointing at. + // + if (auto fieldInfoPtr = _state.fieldCursor) { - SLANG_ASSERT(_state.elementIndex < _state.elementCount); - auto index = _state.elementIndex++; + // We know, because `remainingValueCount` is non-zero, + // that there is at least one more field after this + // one, so we can increment the `fieldInfoPtr` to + // get a pointer to the next field layout in the + // record layout. + // + auto nextFieldInfoPtr = fieldInfoPtr + 1; - auto recordPtr = as<FossilizedRecordVal>(_state.baseValPtr); - return getAddress(recordPtr->getField(index)); - } + // The layout information for a record type stores + // the offset of each field, so we can compute + // the relative offset between two fields as the + // difference between their offsets in the layout. + // + auto offsetToNextField = nextFieldInfoPtr->offset - fieldInfoPtr->offset; - case State::Type::Optional: - { - SLANG_ASSERT(_state.elementCount == 1); - SLANG_ASSERT(_state.elementIndex == 0); + // Adding that relative offset to the data pointer + // will navigate us to the next field. + // + nextDataPtr += offsetToNextField; + _state.fieldCursor = nextFieldInfoPtr; - auto optionalPtr = as<FossilizedOptionalObjBase>(_state.baseValPtr); - return getAddress(optionalPtr->getValue()); - } + // The info for the next field stores a (relative) + // pointer to its data layout, so we can write + // that into our state so that it is available + // for the next read operation. - case State::Type::Array: - case State::Type::Dictionary: + _state.layoutCursor = nextFieldInfoPtr->layout; + } + else { - SLANG_ASSERT(_state.elementIndex < _state.elementCount); - auto index = _state.elementIndex++; - - auto containerPtr = as<FossilizedContainerObjBase>(_state.baseValPtr); - return Fossil::ValPtr(containerPtr->getElement(index)); + // The other case where more than one value can + // be read in the same state is when a collection + // is being read. In that case the setup logic + // will have already stored the stride between + // elements into the state, and we can use that + // to increment the data pointer. + // + // (In the container case the layout pointer doesn't + // need to change, since all of the elements are + // required to have the same layout). + // + auto dataStride = _state.dataStride; + nextDataPtr += dataStride; } - default: - SLANG_UNEXPECTED("unhandled case"); - break; + _state.dataCursor = nextDataPtr; } } -Fossil::AnyValPtr SerialReader::_readIndirectValPtr() +SLANG_FORCE_INLINE Fossil::AnyValPtr SerialReader::_readIndirectValPtr() { auto baseValPtr = _readValPtr(); - auto basePtrPtr = as<FossilizedPtr<void>>(baseValPtr); + auto basePtrPtr = expectNonNullValOfType<FossilizedPtr<void>>(baseValPtr); auto targetValPtr = basePtrPtr->getTargetValPtr(); return targetValPtr; @@ -1663,25 +1520,124 @@ Fossil::AnyValPtr SerialReader::_readIndirectValPtr() Fossil::AnyValPtr SerialReader::_readPotentiallyIndirectValPtr() { - auto baseValPtr = _readValPtr(); - if (auto basePtrPtr = as<FossilizedPtr<void>>(baseValPtr)) + auto stateType = _state.type; + auto valPtr = _readValPtr(); + if (stateType != State::Type::Object) { - auto targetValRef = basePtrPtr->getTargetValRef(); - return Fossil::ValPtr(targetValRef); + auto ptrPtr = expectNonNullValOfType<FossilizedPtr<void>>(valPtr); + valPtr = ptrPtr->getTargetValPtr(); } - return baseValPtr; + return valPtr; } -void SerialReader::_pushState() +void SerialReader::beginTuple(Scope& scope) { - _stack.add(_state); + auto valPtr = _readValPtr(); + auto recordPtr = expectNonNullValOfType<FossilizedRecordVal>(valPtr); + + _pushRecordState(scope, recordPtr); } -void SerialReader::_popState() +void SerialReader::beginOptional(Scope& scope) { - SLANG_ASSERT(_stack.getCount() != 0); - _state = _stack.getLast(); - _stack.removeLast(); + auto valPtr = _readIndirectValPtr(); + auto optionalPtr = expectPossiblyNullValOfType<FossilizedOptionalObjBase>(valPtr); + bool hasValue = optionalPtr->hasValue(); + + _pushState(scope); + + _state.type = State::Type::Object; + if (hasValue) + { + auto heldValPtr = getAddress(optionalPtr->getValue()); + + _state.remainingValueCount = 1; + _state.dataCursor = heldValPtr.getDataPtr(); + _state.layoutCursor = heldValPtr.getLayout(); + } +} + +void SerialReader::handleString(String& value) +{ + auto valPtr = _readPotentiallyIndirectValPtr(); + auto stringPtr = expectPossiblyNullValOfType<FossilizedStringObj>(valPtr); + if (!stringPtr) + { + value = String(); + } + else + { + value = stringPtr->get(); + } +} + +void SerialReader::_pushContainerState( + Scope& scope, + Fossil::ValPtr<FossilizedContainerObjBase> containerObjPtr) +{ + // The elements of a container object start immediately + // at its in-memory address, so we can use the pointer + // to the container object as the pointer to its data. + // + auto containerDataPtr = containerObjPtr.getDataPtr(); + auto containerLayout = containerObjPtr.getLayout(); + + auto elementCount = (uint32_t)containerObjPtr->getElementCount(); + + FossilizedValLayout const* elementLayout = containerLayout->elementLayout; + auto elementStride = containerLayout->elementStride; + + _pushState(scope); + + _state.type = State::Type::Container; + _state.dataCursor = containerDataPtr; + _state.layoutCursor = elementLayout; + _state.dataStride = elementStride; + _state.remainingValueCount = elementCount; +} + +void SerialReader::_pushRecordState(Scope& scope, Fossil::ValPtr<FossilizedRecordVal> recordPtr) +{ + auto recordDataPtr = recordPtr.getDataPtr(); + auto recordLayout = recordPtr.getLayout(); + + auto fieldCount = recordLayout->fieldCount; + + _pushState(scope); + _state.type = State::Type::Record; + _state.dataCursor = recordDataPtr; + _state.remainingValueCount = fieldCount; + if (fieldCount != 0) + { + auto fieldInfo = recordLayout->getField(0); + _state.layoutCursor = fieldInfo->layout; + _state.fieldCursor = fieldInfo; + } +} + + +void SerialReader::beginArray(Scope& scope) +{ + auto valPtr = _readPotentiallyIndirectValPtr(); + auto arrayPtr = expectPossiblyNullValOfType<FossilizedArrayObjBase>(valPtr); + + _pushContainerState(scope, arrayPtr); +} + +void SerialReader::beginDictionary(Scope& scope) +{ + auto valPtr = _readPotentiallyIndirectValPtr(); + auto dictionaryPtr = expectPossiblyNullValOfType<FossilizedDictionaryObjBase>(valPtr); + + _pushContainerState(scope, dictionaryPtr); +} + +void SerialReader::beginStruct(Scope& scope) +{ + auto valPtr = _readValPtr(); + auto recordPtr = expectNonNullValOfType<FossilizedRecordVal>(valPtr); + + _pushRecordState(scope, recordPtr); } } // namespace Fossil diff --git a/source/slang/slang-serialize-fossil.h b/source/slang/slang-serialize-fossil.h index 930719935..184831a39 100644 --- a/source/slang/slang-serialize-fossil.h +++ b/source/slang/slang-serialize-fossil.h @@ -21,8 +21,69 @@ namespace Slang namespace Fossil { +// Deserializing data is an important place where security issues +// can arise, so it is usually important to perform validation +// checks throughout the process, and fail fast rather than +// risk reading mal-formed data. +// +// However, validation typically comes at a performance cost, +// and one of the key cases for serialization in Slang is loading +// the core module from the `slang.dll` binary itself. In order +// to measure how much performance is being lost to validation +// checks, we provide a define that is intended to enable or +// disable validation during deserialization. +// +#define SLANG_SERIALIZE_FOSSIL_ENABLE_VALIDATION_CHECKS 1 + +#if SLANG_SERIALIZE_FOSSIL_ENABLE_VALIDATION_CHECKS +#define SLANG_SERIALIZE_FOSSIL_VALIDATE(CONDITION) \ + do \ + { \ + if (!(CONDITION)) \ + SLANG_UNEXPECTED("invalid format encountered in serialized data"); \ + } while (0) +#else +#define SLANG_SERIALIZE_FOSSIL_VALIDATE(CONDITION) SLANG_ASSERT(CONDITION) +#endif + +// A commonly-occuring kind of validation check when reading +// data in fossil format is asserting that some expected +// piece of data is both *present* and has the expected +// type/layout. +// +template<typename T> +SLANG_FORCE_INLINE ValPtr<T> expectNonNullValOfType(AnyValPtr valPtr) +{ +#if SLANG_SERIALIZE_FOSSIL_ENABLE_VALIDATION_CHECKS + if (auto resultPtr = as<T>(valPtr)) + return resultPtr; + SLANG_UNEXPECTED("invalid format encountered in serialized data"); +#else + return cast<T>(valPtr); +#endif +} + +// Some types can be encoded via a null pointer when they +// are in their "default" state. For example, an empty +// dictionary or array may be encoded as a null pointer +// to a `FossilizedContainerObj`. We still expect a match +// on the *format* in such cases. +// +template<typename T> +SLANG_FORCE_INLINE ValPtr<T> expectPossiblyNullValOfType(AnyValPtr valPtr) +{ +#if SLANG_SERIALIZE_FOSSIL_ENABLE_VALIDATION_CHECKS + auto layout = valPtr.getLayout(); + if (!layout || !T::isMatchingKind(layout->kind)) + { + SLANG_UNEXPECTED("invalid format encountered in serialized data"); + } +#endif + return cast<T>(valPtr); +} + /// Serializer implementation for writing objects to a fossil-format blob. -struct SerialWriter : ISerializerImpl +struct SerialWriter { public: SerialWriter(ChunkBuilder* chunk); @@ -75,12 +136,9 @@ private: { } - virtual ~LayoutObj() {} - FossilizedValKind getKind() const { return kind; } Size getSize() const { return size; } - Size getAlignment() const { return alignment; } FossilizedValKind kind; Size size = 0; @@ -133,6 +191,18 @@ private: class SimpleLayoutObj : public LayoutObj { public: + SimpleLayoutObj(FossilizedValKind kind, Size size, Size alignment) + : LayoutObj(kind, size, alignment) + { + } + + // + // Note that for `SimpleLayoutObj` the default + // alignment is the same as the `size`, while + // for the base `LayoutObj` the default is + // one-byte alignment. + // + SimpleLayoutObj(FossilizedValKind kind, Size size) : LayoutObj(kind, size, size) { @@ -259,7 +329,7 @@ private: LayoutObj* ptrLayout = nullptr; /// Callback information used by the ISerializer interface. - Callback callback = nullptr; + SerializerCallback callback = nullptr; void* context = nullptr; }; @@ -501,62 +571,73 @@ private: void _pushState(LayoutObj* layout); void _popState(); -private: +public: // // The following declarations are the requirements // of the `ISerializerImpl` interface: // - virtual SerializationMode getMode() override; + SerializationMode getMode(); + + void handleBool(bool& value); - virtual void handleBool(bool& value) override; + void handleInt8(int8_t& value); + void handleInt16(int16_t& value); + void handleInt32(Int32& value); + void handleInt64(Int64& value); - virtual void handleInt8(int8_t& value) override; - virtual void handleInt16(int16_t& value) override; - virtual void handleInt32(Int32& value) override; - virtual void handleInt64(Int64& value) override; + void handleUInt8(uint8_t& value); + void handleUInt16(uint16_t& value); + void handleUInt32(UInt32& value); + void handleUInt64(UInt64& value); - virtual void handleUInt8(uint8_t& value) override; - virtual void handleUInt16(uint16_t& value) override; - virtual void handleUInt32(UInt32& value) override; - virtual void handleUInt64(UInt64& value) override; + void handleFloat32(float& value); + void handleFloat64(double& value); - virtual void handleFloat32(float& value) override; - virtual void handleFloat64(double& value) override; + void handleString(String& value); - virtual void handleString(String& value) override; + struct Scope + { + // The `SerialWriter` implementation for fossil + // does not currently take advantage of the `Scope` + // facility provided by the serialization framework. + // + // If we *do* want to modify it to use that facility, + // then we can look to the `SerialReader` implementation + // for an example of how it can be used. + }; - virtual void beginArray() override; - virtual void endArray() override; - virtual void beginOptional() override; - virtual void endOptional() override; + void beginArray(Scope& scope); + void endArray(Scope& scope); - virtual void beginDictionary() override; - virtual void endDictionary() override; + void beginOptional(Scope& scope); + void endOptional(Scope& scope); - virtual bool hasElements() override; + void beginDictionary(Scope& scope); + void endDictionary(Scope& scope); - virtual void beginTuple() override; - virtual void endTuple() override; + bool hasElements(); - virtual void beginStruct() override; - virtual void endStruct() override; + void beginTuple(Scope& scope); + void endTuple(Scope& scope); - virtual void beginVariant() override; - virtual void endVariant() override; + void beginStruct(Scope& scope); + void endStruct(Scope& scope); - virtual void handleFieldKey(char const* name, Int index) override; + void beginVariant(Scope& scope); + void endVariant(Scope& scope); - virtual void handleSharedPtr(void*& value, Callback callback, void* context) override; - virtual void handleUniquePtr(void*& value, Callback callback, void* context) override; + void handleFieldKey(char const* name, Int index); - virtual void handleDeferredObjectContents(void* valuePtr, Callback callback, void* context) - override; + void handleSharedPtr(void*& value, SerializerCallback callback, void* context); + void handleUniquePtr(void*& value, SerializerCallback callback, void* context); + + void handleDeferredObjectContents(void* valuePtr, SerializerCallback callback, void* context); }; /// Serializer implementation for reading objects from a fossil-format blob. -struct SerialReader : ISerializerImpl +struct SerialReader { public: struct ReadContext; @@ -595,57 +676,91 @@ private: /// A state that the reader can be in. struct State { + // Note: the exact order of declaration for the fields + // here can end up impacting the overall performance of + // deserialization, so be mindful when making changes. + /// Type of state; related to the kind of value being read from. /// + /// Most of these values are determined by the `FossilizedValKind` + /// of the parent/container that was pushed to create this state, + /// but the correspondance is not one-to-one. For example, + /// both `FossilizedValKind::ArrayObj` and `::DictinaryObj` map + /// to `State::Type::Container`. + /// + /// Reducing the number of distinct cases tracked here minimizes + /// the complexity of branches on the type of state. + /// enum class Type { - Root, - Array, - Dictionary, - Optional, - Tuple, - Struct, Object, - + Container, + Record, PseudoPtr, }; - /// The type of state. - Type type = Type::Root; + /// The pointers to the data and layout of the fossilized + /// value that will be read next. + /// + void* dataCursor = nullptr; + FossilizedValLayout const* layoutCursor = nullptr; - /// The fossilized value (data and layout) that is being read from. + /// The field layout information for the next value to be read. /// - /// Depending on the `type` of state, this might either be the next value - /// that will be read (e.g., for the `Root` case), or it might be - /// a container that is a parent of the next value to be read. + /// This value is only used when `type == State::Type::Record`. + /// In that case, the `fieldCursor` can be used to locate + /// the layout for the next field (if their is one). /// - Fossil::AnyValPtr baseValPtr; + FossilizedRecordElementLayout const* fieldCursor = nullptr; - /// Index of next element to read. + /// The number of values that can still be read in this state. /// - /// This is used in the case where `baseValue` is some kind of - /// container or record. + /// If the `remainingValueCount` field is zero, then the contents + /// of the remaining fields are irrelevant (and may hold stale + /// values rather than being cleared correctly). /// - Index elementIndex = 0; + uint32_t remainingValueCount = 0; - /// Total number of values that can be read. + /// A stride (in bytes) between values. /// - /// If `baseValue` is a container, this is the element count. - /// If `baseValue` is a tuple/struct, this is the field count. - /// If `baseValue` is an optional, this is either zero or one. - /// If this state is a singleton case like `Root`, will be one. + /// This value is only used when `type == State::Type::Container`. + /// It should be set to zero for any other type of state. /// - Count elementCount = 0; + uint32_t dataStride = 0; + + /// The type of state. + Type type = Type::Object; }; /// The current state. State _state; - /// Stack of saved states. - List<State> _stack; + SLANG_FORCE_INLINE State& getState() { return _state; } + +public: + // + // The serialization protocol allows the back-end + // implementation to define a `Scope` type that will + // be passed into the paired `begin` and `end` operations. + // + // We define a scope type that is able to store a saved + // copy of the state of the `SerialReader`, which allows + // us to maintain a stack of states that gets stored on + // the run-time call stack, instead of requiring a + // heap-allocated container here. + // + + struct Scope + { + private: + friend struct SerialReader; + + State savedState; + }; +private: // - // Like other `ISerializerImpl`s for reading, we track objects + // Like other serializer implementations for reading, we track objects // that are in the process of being read in, to avoid possible // unbounded recursion (and detect circularities when they // occur). @@ -682,12 +797,15 @@ private: State savedState; - Callback callback; + SerializerCallback callback; void* context; }; - void _pushState(); - void _popState(); + void _pushState(Scope& scope); + void _popState(Scope& scope); + + void _pushContainerState(Scope& scope, Fossil::ValPtr<FossilizedContainerObjBase> containerPtr); + void _pushRecordState(Scope& scope, Fossil::ValPtr<FossilizedRecordVal> recordPtr); /// Execute all deferred actions that are still pending. @@ -699,6 +817,10 @@ private: /// Fossil::AnyValPtr _readValPtr(); + /// Advance the read cursor. + /// + void _advanceCursor(); + /// Read an indirect value. /// /// This is the case for things like optionals, that are @@ -717,10 +839,16 @@ private: template<typename T> - void handleSimpleVal(T& value) + SLANG_FORCE_INLINE T _readSimpleVal() { auto valPtr = _readValPtr(); - value = as<Fossilized<T>>(valPtr)->getDataRef(); + return expectNonNullValOfType<Fossilized<T>>(valPtr)->getDataRef(); + } + + template<typename T> + SLANG_FORCE_INLINE void _handleSimpleVal(T& value) + { + value = _readSimpleVal<T>(); } public: @@ -739,62 +867,213 @@ public: }; -private: +public: // // The following declarations are the requirements // of the `ISerializerImpl` interface: // - virtual SerializationMode getMode() override; + SerializationMode getMode(); - virtual void handleBool(bool& value) override; + void handleBool(bool& value); - virtual void handleInt8(int8_t& value) override; - virtual void handleInt16(int16_t& value) override; - virtual void handleInt32(Int32& value) override; - virtual void handleInt64(Int64& value) override; + void handleInt8(int8_t& value); + void handleInt16(int16_t& value); + void handleInt32(Int32& value); + void handleInt64(Int64& value); - virtual void handleUInt8(uint8_t& value) override; - virtual void handleUInt16(uint16_t& value) override; - virtual void handleUInt32(UInt32& value) override; - virtual void handleUInt64(UInt64& value) override; + void handleUInt8(uint8_t& value); + void handleUInt16(uint16_t& value); + void handleUInt32(UInt32& value); + void handleUInt64(UInt64& value); - virtual void handleFloat32(float& value) override; - virtual void handleFloat64(double& value) override; + void handleFloat32(float& value); + void handleFloat64(double& value); - virtual void handleString(String& value) override; + void handleString(String& value); - virtual void beginArray() override; - virtual void endArray() override; + void beginArray(Scope& scope); + void endArray(Scope& scope); - virtual void beginDictionary() override; - virtual void endDictionary() override; + void beginDictionary(Scope& scope); + void endDictionary(Scope& scope); - virtual bool hasElements() override; + bool hasElements(); - virtual void beginStruct() override; - virtual void endStruct() override; + void beginStruct(Scope& scope); + void endStruct(Scope& scope); - virtual void beginVariant() override; - virtual void endVariant() override; + void beginVariant(Scope& scope); + void endVariant(Scope& scope); - virtual void handleFieldKey(char const* name, Int index) override; + void handleFieldKey(char const* name, Int index); - virtual void beginTuple() override; - virtual void endTuple() override; + void beginTuple(Scope& scope); + void endTuple(Scope& scope); - virtual void beginOptional() override; - virtual void endOptional() override; + void beginOptional(Scope& scope); + void endOptional(Scope& scope); - virtual void handleSharedPtr(void*& value, Callback callback, void* context) override; - virtual void handleUniquePtr(void*& value, Callback callback, void* context) override; + void handleSharedPtr(void*& value, SerializerCallback callback, void* context); + void handleUniquePtr(void*& value, SerializerCallback callback, void* context); - virtual void handleDeferredObjectContents(void* valuePtr, Callback callback, void* context) - override; + void handleDeferredObjectContents(void* valuePtr, SerializerCallback callback, void* context); }; using ReadContext = SerialReader::ReadContext; +SLANG_FORCE_INLINE Fossil::AnyValPtr SerialReader::readValPtr() +{ + return _readValPtr(); +} + +SLANG_FORCE_INLINE SerializationMode SerialReader::getMode() +{ + return SerializationMode::Read; +} + +SLANG_FORCE_INLINE void SerialReader::handleBool(bool& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleInt8(int8_t& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleInt16(int16_t& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleInt32(Int32& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleInt64(Int64& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleUInt8(uint8_t& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleUInt16(uint16_t& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleUInt32(UInt32& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleUInt64(UInt64& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleFloat32(float& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::handleFloat64(double& value) +{ + _handleSimpleVal(value); +} + +SLANG_FORCE_INLINE void SerialReader::endArray(Scope& scope) +{ + _popState(scope); +} + +SLANG_FORCE_INLINE void SerialReader::endDictionary(Scope& scope) +{ + _popState(scope); +} + +SLANG_FORCE_INLINE bool SerialReader::hasElements() +{ + return getState().remainingValueCount != 0; +} + +SLANG_FORCE_INLINE void SerialReader::endStruct(Scope& scope) +{ + _popState(scope); +} + +SLANG_FORCE_INLINE void SerialReader::endVariant(Scope& scope) +{ + _popState(scope); +} + +SLANG_FORCE_INLINE void SerialReader::handleFieldKey(char const* name, Int index) +{ + // For now we are ignoring field keys, and treating + // structs as basically equivalent to tuples. + SLANG_UNUSED(name); + SLANG_UNUSED(index); +} + +SLANG_FORCE_INLINE void SerialReader::endTuple(Scope& scope) +{ + _popState(scope); +} + +SLANG_FORCE_INLINE void SerialReader::endOptional(Scope& scope) +{ + _popState(scope); +} + +SLANG_FORCE_INLINE void SerialReader::handleUniquePtr( + void*& value, + SerializerCallback callback, + void* context) +{ + // We treat all pointers as shared pointers, because there isn't really + // an optimized representation we would want to use for the unique case. + // + handleSharedPtr(value, callback, context); +} + +SLANG_FORCE_INLINE void SerialReader::_pushState(Scope& scope) +{ + scope.savedState = _state; + _state = State(); +} + +SLANG_FORCE_INLINE void SerialReader::_popState(Scope& scope) +{ + _state = scope.savedState; +} + +SLANG_FORCE_INLINE Fossil::AnyValPtr SerialReader::_readValPtr() +{ + SLANG_ASSERT(_state.remainingValueCount > 0); + + // The rest of the `SerialReader` implementation conspires + // to set things up so that the `dataCursor` and `layoutCursor` + // stored in `_state` will always represent the next value + // to be read, so the logic to determine the result of + // this function is trivial. + // + auto dataPtr = _state.dataCursor; + auto layoutPtr = _state.layoutCursor; + + // Currently, the logic to advance the cursor(s) to the next + // value is more complicated, and thus it isn't included in + // the inlined part of the function. + // + _advanceCursor(); + + return AnyValPtr(dataPtr, layoutPtr); +} + } // namespace Fossil } // namespace Slang diff --git a/source/slang/slang-serialize-ir.cpp b/source/slang/slang-serialize-ir.cpp index 52ec0e3f2..594953d7e 100644 --- a/source/slang/slang-serialize-ir.cpp +++ b/source/slang/slang-serialize-ir.cpp @@ -4,6 +4,7 @@ #include "core/slang-blob-builder.h" #include "core/slang-common.h" #include "core/slang-dictionary.h" +#include "core/slang-performance-profiler.h" #include "slang-ir-insts-stable-names.h" #include "slang-ir-insts.h" #include "slang-ir-validate.h" @@ -269,11 +270,11 @@ struct IRSerialWriteContext; // Specialize to the reader/writer for the specific backend we're targeting // instead of ISerializerImpl to avoid some virtual function calls #if USE_RIFF -using IRWriteSerializer = Serializer_<RIFFSerialWriter, IRSerialWriteContext>; -using IRReadSerializer = Serializer_<RIFFSerialReader, IRSerialReadContext>; +using IRWriteSerializer = Serializer<RIFFSerialWriter, IRSerialWriteContext>; +using IRReadSerializer = Serializer<RIFFSerialReader, IRSerialReadContext>; #else -using IRWriteSerializer = Serializer_<Fossil::SerialWriter, IRSerialWriteContext>; -using IRReadSerializer = Serializer_<Fossil::SerialReader, IRSerialReadContext>; +using IRWriteSerializer = Serializer<Fossil::SerialWriter, IRSerialWriteContext>; +using IRReadSerializer = Serializer<Fossil::SerialReader, IRSerialReadContext>; #endif struct IRSerialWriteContext : SourceLocSerialContext @@ -848,6 +849,8 @@ Result readSerializedModuleIR( SerialSourceLocReader* sourceLocReader, RefPtr<IRModule>& outIRModule) { + SLANG_PROFILE; + SLANG_RETURN_ON_FAIL(readSerializedModuleIR_(chunk, session, sourceLocReader, outIRModule)); // diff --git a/source/slang/slang-serialize-riff.cpp b/source/slang/slang-serialize-riff.cpp index fb8acd2bd..ff16dbf53 100644 --- a/source/slang/slang-serialize-riff.cpp +++ b/source/slang/slang-serialize-riff.cpp @@ -144,22 +144,22 @@ void RIFFSerialWriter::_writeFloat(double value) } } -void RIFFSerialWriter::beginArray() +void RIFFSerialWriter::beginArray(Scope&) { _cursor.beginListChunk(RIFFSerial::kArrayFourCC); } -void RIFFSerialWriter::endArray() +void RIFFSerialWriter::endArray(Scope&) { _cursor.endChunk(); } -void RIFFSerialWriter::beginDictionary() +void RIFFSerialWriter::beginDictionary(Scope&) { _cursor.beginListChunk(RIFFSerial::kDictionaryFourCC); } -void RIFFSerialWriter::endDictionary() +void RIFFSerialWriter::endDictionary(Scope&) { _cursor.endChunk(); } @@ -169,24 +169,24 @@ bool RIFFSerialWriter::hasElements() return false; } -void RIFFSerialWriter::beginStruct() +void RIFFSerialWriter::beginStruct(Scope&) { _cursor.beginListChunk(RIFFSerial::kStructFourCC); } -void RIFFSerialWriter::endStruct() +void RIFFSerialWriter::endStruct(Scope&) { _cursor.endChunk(); } -void RIFFSerialWriter::beginVariant() +void RIFFSerialWriter::beginVariant(Scope& scope) { - beginStruct(); + beginStruct(scope); } -void RIFFSerialWriter::endVariant() +void RIFFSerialWriter::endVariant(Scope& scope) { - endStruct(); + endStruct(scope); } void RIFFSerialWriter::handleFieldKey(char const* name, Int index) @@ -197,27 +197,27 @@ void RIFFSerialWriter::handleFieldKey(char const* name, Int index) SLANG_UNUSED(index); } -void RIFFSerialWriter::beginTuple() +void RIFFSerialWriter::beginTuple(Scope&) { _cursor.beginListChunk(RIFFSerial::kTupleFourCC); } -void RIFFSerialWriter::endTuple() +void RIFFSerialWriter::endTuple(Scope&) { _cursor.endChunk(); } -void RIFFSerialWriter::beginOptional() +void RIFFSerialWriter::beginOptional(Scope&) { _cursor.beginListChunk(RIFFSerial::kOptionalFourCC); } -void RIFFSerialWriter::endOptional() +void RIFFSerialWriter::endOptional(Scope&) { _cursor.endChunk(); } -void RIFFSerialWriter::handleSharedPtr(void*& value, Callback callback, void* context) +void RIFFSerialWriter::handleSharedPtr(void*& value, SerializerCallback callback, void* context) { // Because we are writing, we only care about the // pointer that is already present in `value`. @@ -283,7 +283,7 @@ void RIFFSerialWriter::handleSharedPtr(void*& value, Callback callback, void* co _objects.add(objectInfo); } -void RIFFSerialWriter::handleUniquePtr(void*& value, Callback callback, void* context) +void RIFFSerialWriter::handleUniquePtr(void*& value, SerializerCallback callback, void* context) { // We treat all pointers as shared pointers, because there isn't really // an optimized representation we would want to use for the unique case. @@ -293,7 +293,7 @@ void RIFFSerialWriter::handleUniquePtr(void*& value, Callback callback, void* co void RIFFSerialWriter::handleDeferredObjectContents( void* valuePtr, - Callback callback, + SerializerCallback callback, void* context) { // Because we are already deferring writing of the *entirety* of @@ -485,23 +485,22 @@ void RIFFSerialReader::handleString(String& value) _advanceCursor(); } -void RIFFSerialReader::beginArray() +void RIFFSerialReader::beginArray(Scope&) { _beginListChunk(RIFFSerial::kArrayFourCC); } -void RIFFSerialReader::endArray() +void RIFFSerialReader::endArray(Scope&) { _endListChunk(); } - -void RIFFSerialReader::beginDictionary() +void RIFFSerialReader::beginDictionary(Scope&) { _beginListChunk(RIFFSerial::kDictionaryFourCC); } -void RIFFSerialReader::endDictionary() +void RIFFSerialReader::endDictionary(Scope&) { _endListChunk(); } @@ -511,24 +510,24 @@ bool RIFFSerialReader::hasElements() return _cursor.get() != nullptr; } -void RIFFSerialReader::beginStruct() +void RIFFSerialReader::beginStruct(Scope&) { _beginListChunk(RIFFSerial::kStructFourCC); } -void RIFFSerialReader::endStruct() +void RIFFSerialReader::endStruct(Scope&) { _endListChunk(); } -void RIFFSerialReader::beginVariant() +void RIFFSerialReader::beginVariant(Scope& scope) { - beginStruct(); + beginStruct(scope); } -void RIFFSerialReader::endVariant() +void RIFFSerialReader::endVariant(Scope& scope) { - endStruct(); + endStruct(scope); } void RIFFSerialReader::handleFieldKey(char const* name, Int index) @@ -539,22 +538,22 @@ void RIFFSerialReader::handleFieldKey(char const* name, Int index) SLANG_UNUSED(index); } -void RIFFSerialReader::beginTuple() +void RIFFSerialReader::beginTuple(Scope&) { _beginListChunk(RIFFSerial::kTupleFourCC); } -void RIFFSerialReader::endTuple() +void RIFFSerialReader::endTuple(Scope&) { _endListChunk(); } -void RIFFSerialReader::beginOptional() +void RIFFSerialReader::beginOptional(Scope&) { _beginListChunk(RIFFSerial::kOptionalFourCC); } -void RIFFSerialReader::endOptional() +void RIFFSerialReader::endOptional(Scope&) { _endListChunk(); } @@ -572,7 +571,7 @@ RIFFSerialReader::ObjectIndex RIFFSerialReader::_readObjectReference() return objectIndex; } -void RIFFSerialReader::handleSharedPtr(void*& value, Callback callback, void* context) +void RIFFSerialReader::handleSharedPtr(void*& value, SerializerCallback callback, void* context) { // The logic here largely mirrors what appears in // `RIFFSerialWriter::handleSharedPtr`. @@ -695,7 +694,7 @@ void RIFFSerialReader::handleSharedPtr(void*& value, Callback callback, void* co value = objectInfo.ptr; } -void RIFFSerialReader::handleUniquePtr(void*& value, Callback callback, void* userData) +void RIFFSerialReader::handleUniquePtr(void*& value, SerializerCallback callback, void* userData) { // We treat all pointers as shared pointers, because there isn't really // an optimized representation we would want to use for the unique case. @@ -705,7 +704,7 @@ void RIFFSerialReader::handleUniquePtr(void*& value, Callback callback, void* us void RIFFSerialReader::handleDeferredObjectContents( void* valuePtr, - Callback callback, + SerializerCallback callback, void* context) { // Unlike the case in `RIFFSerialWriter::handleDeferredObjectContents()`, diff --git a/source/slang/slang-serialize-riff.h b/source/slang/slang-serialize-riff.h index 256c185d4..b305113fa 100644 --- a/source/slang/slang-serialize-riff.h +++ b/source/slang/slang-serialize-riff.h @@ -118,7 +118,7 @@ static const FourCC::RawValue kObjectDefinitionListFourCC = SLANG_FOUR_CC('o', ' } // namespace RIFFSerial /// Serializer implementation for writing to a tree of RIFF chunks. -struct RIFFSerialWriter : ISerializerImpl +struct RIFFSerialWriter { public: /// Construct a writer to append to the given RIFF `chunk`. @@ -160,7 +160,7 @@ private: void* ptr; /// Callback that can be invoked to serialize the object's data. - Callback callback; + SerializerCallback callback; /// Context pointer for `callback` void* context; @@ -196,56 +196,62 @@ private: // of the `ISerializerImpl` interface: // - virtual SerializationMode getMode() override; + SerializationMode getMode(); - virtual void handleBool(bool& value) override; + void handleBool(bool& value); - virtual void handleInt8(int8_t& value) override; - virtual void handleInt16(int16_t& value) override; - virtual void handleInt32(Int32& value) override; - virtual void handleInt64(Int64& value) override; + void handleInt8(int8_t& value); + void handleInt16(int16_t& value); + void handleInt32(Int32& value); + void handleInt64(Int64& value); - virtual void handleUInt8(uint8_t& value) override; - virtual void handleUInt16(uint16_t& value) override; - virtual void handleUInt32(UInt32& value) override; - virtual void handleUInt64(UInt64& value) override; + void handleUInt8(uint8_t& value); + void handleUInt16(uint16_t& value); + void handleUInt32(UInt32& value); + void handleUInt64(UInt64& value); - virtual void handleFloat32(float& value) override; - virtual void handleFloat64(double& value) override; + void handleFloat32(float& value); + void handleFloat64(double& value); - virtual void handleString(String& value) override; + void handleString(String& value); - virtual void beginArray() override; - virtual void endArray() override; + struct Scope + { + // The RIFF serialization back-end is currently + // not taking advantage of the `Scope` facility + // in the serialization framework. + }; + + void beginArray(Scope&); + void endArray(Scope&); - virtual void beginDictionary() override; - virtual void endDictionary() override; + void beginDictionary(Scope&); + void endDictionary(Scope&); - virtual bool hasElements() override; + bool hasElements(); - virtual void beginStruct() override; - virtual void endStruct() override; + void beginStruct(Scope&); + void endStruct(Scope&); - virtual void beginVariant() override; - virtual void endVariant() override; + void beginVariant(Scope&); + void endVariant(Scope&); - virtual void handleFieldKey(char const* name, Int index) override; + void handleFieldKey(char const* name, Int index); - virtual void beginTuple() override; - virtual void endTuple() override; + void beginTuple(Scope&); + void endTuple(Scope&); - virtual void beginOptional() override; - virtual void endOptional() override; + void beginOptional(Scope&); + void endOptional(Scope&); - virtual void handleSharedPtr(void*& value, Callback callback, void* context) override; - virtual void handleUniquePtr(void*& value, Callback callback, void* context) override; + void handleSharedPtr(void*& value, SerializerCallback callback, void* context); + void handleUniquePtr(void*& value, SerializerCallback callback, void* context); - virtual void handleDeferredObjectContents(void* valuePtr, Callback callback, void* context) - override; + void handleDeferredObjectContents(void* valuePtr, SerializerCallback callback, void* context); }; /// Serializer implementation for reading from a tree of RIFF chunks. -struct RIFFSerialReader : ISerializerImpl +struct RIFFSerialReader { public: /// Construct a reader to read data from the given `chunk`. @@ -355,7 +361,7 @@ private: Cursor savedCursor; /// The callback to apply to read data into the `valuePtr` - Callback callback; + SerializerCallback callback; /// The context pointer for the `callback`. void* context; @@ -399,52 +405,58 @@ private: // of the `ISerializerImpl` interface: // - virtual SerializationMode getMode() override; + SerializationMode getMode(); - virtual void handleBool(bool& value) override; + void handleBool(bool& value); - virtual void handleInt8(int8_t& value) override; - virtual void handleInt16(int16_t& value) override; - virtual void handleInt32(Int32& value) override; - virtual void handleInt64(Int64& value) override; + void handleInt8(int8_t& value); + void handleInt16(int16_t& value); + void handleInt32(Int32& value); + void handleInt64(Int64& value); - virtual void handleUInt8(uint8_t& value) override; - virtual void handleUInt16(uint16_t& value) override; - virtual void handleUInt32(UInt32& value) override; - virtual void handleUInt64(UInt64& value) override; + void handleUInt8(uint8_t& value); + void handleUInt16(uint16_t& value); + void handleUInt32(UInt32& value); + void handleUInt64(UInt64& value); - virtual void handleFloat32(float& value) override; - virtual void handleFloat64(double& value) override; + void handleFloat32(float& value); + void handleFloat64(double& value); - virtual void handleString(String& value) override; + void handleString(String& value); + + struct Scope + { + // The RIFF serialization back-end is currently + // not taking advantage of the `Scope` facility + // in the serialization framework. + }; - virtual void beginArray() override; - virtual void endArray() override; + void beginArray(Scope&); + void endArray(Scope&); - virtual void beginDictionary() override; - virtual void endDictionary() override; + void beginDictionary(Scope&); + void endDictionary(Scope&); - virtual bool hasElements() override; + bool hasElements(); - virtual void beginStruct() override; - virtual void endStruct() override; + void beginStruct(Scope&); + void endStruct(Scope&); - virtual void beginVariant() override; - virtual void endVariant() override; + void beginVariant(Scope&); + void endVariant(Scope&); - virtual void handleFieldKey(char const* name, Int index) override; + void handleFieldKey(char const* name, Int index); - virtual void beginTuple() override; - virtual void endTuple() override; + void beginTuple(Scope&); + void endTuple(Scope&); - virtual void beginOptional() override; - virtual void endOptional() override; + void beginOptional(Scope&); + void endOptional(Scope&); - virtual void handleSharedPtr(void*& value, Callback callback, void* context) override; - virtual void handleUniquePtr(void*& value, Callback callback, void* context) override; + void handleSharedPtr(void*& value, SerializerCallback callback, void* context); + void handleUniquePtr(void*& value, SerializerCallback callback, void* context); - virtual void handleDeferredObjectContents(void* valuePtr, Callback callback, void* context) - override; + void handleDeferredObjectContents(void* valuePtr, SerializerCallback callback, void* context); }; } // namespace Slang diff --git a/source/slang/slang-serialize.h b/source/slang/slang-serialize.h index 261f3f2d6..4aad5a4f2 100644 --- a/source/slang/slang-serialize.h +++ b/source/slang/slang-serialize.h @@ -21,26 +21,11 @@ // SomeObject* object; // }; // -// then you can declare serialization support for your type +// then you can implement serialization support for your type // with something like: // -// // my-thing.h -// ... -// #include "slang-serialize.h" -// ... -// -// struct MyThing { ... } -// -// void serialize(Serializer const& serializer, MyThing& value); -// -// and then implement that support with something like: -// -// // my-thing.cpp -// #include "my-thing.h" -// -// ... -// -// void serialize(Serializer const& serializer, MyThing& value) +// template<typename S> +// void serialize(S const& serializer, MyThing& value) // { // SLANG_SCOPED_SERIALIZER_STRUCT(serializer); // serialize(serializer, value.a); @@ -81,17 +66,56 @@ enum class SerializationMode }; // -// In order to support different serialized formats, and to -// abstract over the difference between reading and writing, -// we define a base interface for serialization. This interface -// is somewhat user-unfriendly, and is *not* intended for -// ordinary code to interface with directly. +// Complex pointer-based object graphs can be challenging for +// a serialization system. Naively written recursive serialization +// logic can lead to extremely deep call graphs at runtime +// (potentially overflowing the stack), and when deserialization +// needs to interact with systems for caching and/or deduplicating +// objects, the ordering constraints imposed by those systems +// must be respected. +// +// This serialization system relies on callback functions in +// a few places to allow serialization actions to be queued up or +// otherwise rescheduled to avoid these problems. +// + +/// Callback type used by the serialization infrastructure. +/// +/// Users typically will never define callbacks manually; the +/// system creates and manages them behind the scenes, based +/// on user-defined types. +/// +typedef void (*SerializerCallback)(void* valuePtr, void* impl, void* context); + +// +// In order to achieve the best possible performance, +// serialization functions need to be statically specialized +// to the particular data format being used. To that +// end, concrete serialization formats are expected to +// model the right concept (provide the right methods, +// type declarations, etc.), but there is not a single +// base class or interface that they all *must* inherit from. +// +// However, to help illustrate the requirements for a +// complete serializer implementation, we will declare an +// interface that helps illustrate the outline of what +// a serializer implementation needs to provide. +// +// Note: this interface is somewhat user-unfriendly. It +// is *not* intended to be something that user code would +// usually interact with directly, and instead is intended +// to define only the operations that a serialization +// back-end must support. // /// Base interface for serialization. /// /// Can be used for both reading and writing of serialized data. /// +/// Note that implementations of serialization back-ends do +/// *not* need to inherit from this type; it currently serves +/// only to define the requirements. +/// struct ISerializerImpl { /// Get the mode that this serializer is operating in (reading or writing). @@ -217,6 +241,35 @@ struct ISerializerImpl /// virtual void handleString(String& value) = 0; + // + // Every concrete serializer implementation should define + // its own `Scope` type, which will be passed as the + // operand to the various paired `begin`/`end` operations + // below. Code that is calling into a serializer takes + // responsibility for maintaining the `Scope` for the + // duration of the `begin`/`end` pair, and passing the + // same data in to `end` that was passed into `begin`, + // without making any modifications of its own. + // + // An implementation may be able to use the `Scope` to + // store information related to the hierarchy of `begin` + // and `end` operations, and thus avoid needing to + // maintain its own heap-allocated stack structure. + // + struct Scope + { + // The implementation of `Scope` in this placeholder + // interface is intended to define a kind of upper + // bound on the size of reasonable scope representations. + // + // The intention is that any concrete implementation + // could be wrapped up as an `ISerializerImpl`, so + // long as its `Scope` fits within the storage limit + // defined here. + // + void* storage[8]; + }; + /// Begin serializing an array value. /// /// An array should be used to serialize an @@ -235,10 +288,10 @@ struct ISerializerImpl /// and serialize values in a loop until `hasElements()` /// returns `false`. /// - virtual void beginArray() = 0; + virtual void beginArray(Scope&) = 0; /// End serializing an array value. - virtual void endArray() = 0; + virtual void endArray(Scope&) = 0; /// Begin serializing an optional value. /// @@ -258,10 +311,10 @@ struct ISerializerImpl /// test whether the serialized optional has a value and, /// if it does, read the value before calling `endOptional()`. /// - virtual void beginOptional() = 0; + virtual void beginOptional(Scope&) = 0; /// End serializing an optional value. - virtual void endOptional() = 0; + virtual void endOptional(Scope&) = 0; /// Begin serializing a dictionary value. /// @@ -285,10 +338,10 @@ struct ISerializerImpl /// and serialize values in a loop until `hasElements()` /// returns `false`. /// - virtual void beginDictionary() = 0; + virtual void beginDictionary(Scope&) = 0; /// End serializing a dictionary value. - virtual void endDictionary() = 0; + virtual void endDictionary(Scope&) = 0; /// Check whether there are elements remaining to be read /// from a serialized container. @@ -309,10 +362,10 @@ struct ISerializerImpl /// to `hasElements()` are allowed between `beginTuple()` /// and `endTuple()`. /// - virtual void beginTuple() = 0; + virtual void beginTuple(Scope&) = 0; /// End serializing a tuple value. - virtual void endTuple() = 0; + virtual void endTuple(Scope&) = 0; /// Begin serializing a struct value. @@ -330,10 +383,10 @@ struct ISerializerImpl /// they were written, and how to handle attempts /// to read a field that was not written. /// - virtual void beginStruct() = 0; + virtual void beginStruct(Scope&) = 0; /// End serializing a struct value. - virtual void endStruct() = 0; + virtual void endStruct(Scope&) = 0; /// Begin serializing a variant value. /// @@ -354,10 +407,10 @@ struct ISerializerImpl /// struct always including the same members in the same /// order. /// - virtual void beginVariant() = 0; + virtual void beginVariant(Scope&) = 0; /// End serializing a variant value. - virtual void endVariant() = 0; + virtual void endVariant(Scope&) = 0; /// Set the key for the next struct field to be serialized. /// @@ -433,11 +486,12 @@ struct ISerializerImpl }; // -// Rather than interact with instances of `ISerializerImpl` directly, +// Rather than interface with serialization back-ends directly, // most client code will use a wrapper type that amounts to a kind // of smart pointer. // -// While the `ISerializerImpl` interface can cover a wide range of +// While serialization back-ends providing operations comparable to +// `ISerializerImpl` above can cover a wide range of // types that need to be serialized, it is common for types to require // more specific *context* to be available in order to perform serialization. // For example, code might need access to a factory object in order @@ -451,35 +505,40 @@ struct ISerializerImpl /// Base type for serialization contexts. /// -/// The type parameter `Impl` should be a type that derives from -/// `ISerializerImpl`, and the `Context` type parameter can be any -/// type that passes along additional context information needed. +/// The type parameter `I` should be the serialization back-end +/// implementation (e.g., `ISerializerImpl`), while the `C` type +/// parameter can be any type that carries additional context +/// information needed. /// -template<typename Impl, typename Context> -struct SerializerBase +template<typename I, typename C> +struct Serializer { public: - SerializerBase() = default; - SerializerBase(Impl* impl, Context* context = nullptr) + using Impl = I; + using Context = C; + + SLANG_FORCE_INLINE Serializer() = default; + SLANG_FORCE_INLINE Serializer(Impl* impl, Context* context = nullptr) : _impl(impl), _context(context) { } - template<typename I, typename C> - SerializerBase( - SerializerBase<I, C> const& serializer, + template<typename SourceImpl, typename SourceContext> + SLANG_FORCE_INLINE Serializer( + Serializer<SourceImpl, SourceContext> const& serializer, std::enable_if_t< - std::is_convertible_v<I*, Impl*> && std::is_convertible_v<C*, Context*>, + std::is_convertible_v<SourceImpl*, Impl*> && + std::is_convertible_v<SourceContext*, Context*>, void>* = nullptr) : _impl(serializer.getImpl()), _context(serializer.getContext()) { } - Impl* getImpl() const { return _impl; } - Context* getContext() const { return _context; } + SLANG_FORCE_INLINE Impl* getImpl() const { return _impl; } + SLANG_FORCE_INLINE Context* getContext() const { return _context; } - Impl* get() const { return _impl; } - Impl* operator->() const { return get(); } + SLANG_FORCE_INLINE Impl* get() const { return _impl; } + SLANG_FORCE_INLINE Impl* operator->() const { return get(); } private: @@ -487,113 +546,109 @@ private: Context* _context = nullptr; }; -/// A serialization context. -/// -/// The type parameter `Impl` should be a type that derives from -/// `ISerializerImpl`, and the `Context` type parameter can be any -/// type that passes along additional context information needed. -/// -template<typename Impl, typename Context> -struct Serializer_ : SerializerBase<Impl, Context> -{ - using SerializerBase<Impl, Context>::SerializerBase; -}; - -/// Default serialization context. -using Serializer = Serializer_<ISerializerImpl, void>; - // // We define namespace-scope functions that mirror some -// of the operations of `ISerializerImpl`, so that they -// can be invoked on any type that is contextually -// convertible to a `Serializer`. This allows users -// to define their own serialization context types while -// still being able to take advantage of the utility -// operations in this file for serializing basic types, -// arrays, dictionaries, etc. +// of the operations of `ISerializerImpl`, to insulate +// user-defined `serialize()` function implementations +// a bit from the details of the serializer concept/interface. // - /// Get the mode of `serializer`. -inline SerializationMode getMode(Serializer const& serializer) +template<typename S> +SLANG_FORCE_INLINE SerializationMode getMode(S const& serializer) { return serializer->getMode(); } /// Check if `serializer` is reading serialized data. -inline bool isReading(Serializer const& serializer) +template<typename S> +SLANG_FORCE_INLINE bool isReading(S const& serializer) { return getMode(serializer) == SerializationMode::Read; } /// Check if `serializer` is writing serialized data. -inline bool isWriting(Serializer const& serializer) +template<typename S> +SLANG_FORCE_INLINE bool isWriting(S const& serializer) { return getMode(serializer) == SerializationMode::Write; } /// Check if `serializer` has more container elements. -inline bool hasElements(Serializer const& serializer) +template<typename S> +SLANG_FORCE_INLINE bool hasElements(S const& serializer) { return serializer->hasElements(); } -inline void serialize(Serializer const& serializer, bool& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, bool& value) { serializer->handleBool(value); } -inline void serialize(Serializer const& serializer, int8_t& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, int8_t& value) { serializer->handleInt8(value); } -inline void serialize(Serializer const& serializer, int16_t& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, int16_t& value) { serializer->handleInt16(value); } -inline void serialize(Serializer const& serializer, Int32& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, Int32& value) { serializer->handleInt32(value); } -inline void serialize(Serializer const& serializer, Int64& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, Int64& value) { serializer->handleInt64(value); } -inline void serialize(Serializer const& serializer, uint8_t& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, uint8_t& value) { serializer->handleUInt8(value); } -inline void serialize(Serializer const& serializer, uint16_t& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, uint16_t& value) { serializer->handleUInt16(value); } -inline void serialize(Serializer const& serializer, UInt32& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, UInt32& value) { serializer->handleUInt32(value); } -inline void serialize(Serializer const& serializer, UInt64& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, UInt64& value) { serializer->handleUInt64(value); } -inline void serialize(Serializer const& serializer, float& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, float& value) { serializer->handleFloat32(value); } -inline void serialize(Serializer const& serializer, double& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, double& value) { serializer->handleFloat64(value); } -inline void serialize(Serializer const& serializer, String& value) +template<typename S> +SLANG_FORCE_INLINE void serialize(S const& serializer, String& value) { serializer->handleString(value); } @@ -604,8 +659,8 @@ inline void serialize(Serializer const& serializer, String& value) /// converting it to/from the given `RawType` for storage /// in the serialized format. /// -template<typename RawType = Int32, typename EnumType> -void serializeEnum(Serializer const& serializer, EnumType& value) +template<typename RawType = Int32, typename S, typename EnumType> +SLANG_FORCE_INLINE void serializeEnum(S const& serializer, EnumType& value) { auto raw = RawType(value); serialize(serializer, raw); @@ -619,136 +674,152 @@ void serializeEnum(Serializer const& serializer, EnumType& value) // each of those types we define a macro to simplify // introducing a coresponding scope. // +// These types handle the details of properly allocating +// and retaining a `Scope` value appropriate to the +// serializer implementation, and passing it into both +// the `begin` and `end` operations. +// +template<typename S> struct ScopedSerializerArray { public: - ScopedSerializerArray(Serializer const& serializer) + SLANG_FORCE_INLINE ScopedSerializerArray(S const& serializer) : _serializer(serializer) { - serializer->beginArray(); + serializer->beginArray(_scope); } - ~ScopedSerializerArray() { _serializer->endArray(); } + SLANG_FORCE_INLINE ~ScopedSerializerArray() { _serializer->endArray(_scope); } private: - Serializer _serializer; + S _serializer; + typename S::Impl::Scope _scope; }; +template<typename S> struct ScopedSerializerDictionary { public: - ScopedSerializerDictionary(Serializer const& serializer) + SLANG_FORCE_INLINE ScopedSerializerDictionary(S const& serializer) : _serializer(serializer) { - serializer->beginDictionary(); + serializer->beginDictionary(_scope); } - ~ScopedSerializerDictionary() { _serializer->endDictionary(); } + SLANG_FORCE_INLINE ~ScopedSerializerDictionary() { _serializer->endDictionary(_scope); } private: - Serializer _serializer; + S _serializer; + typename S::Impl::Scope _scope; }; +template<typename S> struct ScopedSerializerStruct { public: - ScopedSerializerStruct(Serializer const& serializer) + SLANG_FORCE_INLINE ScopedSerializerStruct(S const& serializer) : _serializer(serializer) { - serializer->beginStruct(); + serializer->beginStruct(_scope); } - ~ScopedSerializerStruct() { _serializer->endStruct(); } + SLANG_FORCE_INLINE ~ScopedSerializerStruct() { _serializer->endStruct(_scope); } private: - Serializer _serializer; + S _serializer; + typename S::Impl::Scope _scope; }; +template<typename S> struct ScopedSerializerVariant { public: - ScopedSerializerVariant(Serializer const& serializer) + SLANG_FORCE_INLINE ScopedSerializerVariant(S const& serializer) : _serializer(serializer) { - serializer->beginVariant(); + serializer->beginVariant(_scope); } - ~ScopedSerializerVariant() { _serializer->endVariant(); } + SLANG_FORCE_INLINE ~ScopedSerializerVariant() { _serializer->endVariant(_scope); } private: - Serializer _serializer; + S _serializer; + typename S::Impl::Scope _scope; }; +template<typename S> struct ScopedSerializerTuple { public: - ScopedSerializerTuple(Serializer const& serializer) + SLANG_FORCE_INLINE ScopedSerializerTuple(S const& serializer) : _serializer(serializer) { - serializer->beginTuple(); + serializer->beginTuple(_scope); } - ~ScopedSerializerTuple() { _serializer->endTuple(); } + SLANG_FORCE_INLINE ~ScopedSerializerTuple() { _serializer->endTuple(_scope); } private: - Serializer _serializer; + S _serializer; + typename S::Impl::Scope _scope; }; +template<typename S> struct ScopedSerializerOptional { public: - ScopedSerializerOptional(Serializer const& serializer) + SLANG_FORCE_INLINE ScopedSerializerOptional(S const& serializer) : _serializer(serializer) { - serializer->beginOptional(); + serializer->beginOptional(_scope); } - ~ScopedSerializerOptional() { _serializer->endOptional(); } + SLANG_FORCE_INLINE ~ScopedSerializerOptional() { _serializer->endOptional(_scope); } private: - Serializer _serializer; + S _serializer; + typename S::Impl::Scope _scope; }; -#define SLANG_SCOPED_SERIALIZER_ARRAY(SERIALIZER) \ - ::Slang::ScopedSerializerArray SLANG_CONCAT(_scopedSerializerArray, __LINE__)(SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_ARRAY(SERIALIZER) \ + ::Slang::ScopedSerializerArray<std::remove_reference_t<decltype(SERIALIZER)>> SLANG_CONCAT( \ + _scopedSerializerArray, \ + __LINE__)(SERIALIZER) -#define SLANG_SCOPED_SERIALIZER_DICTIONARY(SERIALIZER) \ - ::Slang::ScopedSerializerDictionary SLANG_CONCAT(_scopedSerializerDictionary, __LINE__)( \ - SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_DICTIONARY(SERIALIZER) \ + ::Slang::ScopedSerializerDictionary<std::remove_reference_t<decltype(SERIALIZER)>> \ + SLANG_CONCAT(_scopedSerializerDictionary, __LINE__)(SERIALIZER) -#define SLANG_SCOPED_SERIALIZER_OPTIONAL(SERIALIZER) \ - ::Slang::ScopedSerializerOptional SLANG_CONCAT(_scopedSerializerOptional, __LINE__)(SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_OPTIONAL(SERIALIZER) \ + ::Slang::ScopedSerializerOptional<std::remove_reference_t<decltype(SERIALIZER)>> SLANG_CONCAT( \ + _scopedSerializerOptional, \ + __LINE__)(SERIALIZER) -#define SLANG_SCOPED_SERIALIZER_STRUCT(SERIALIZER) \ - ::Slang::ScopedSerializerStruct SLANG_CONCAT(_scopedSerializerStruct, __LINE__)(SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_STRUCT(SERIALIZER) \ + ::Slang::ScopedSerializerStruct<std::remove_reference_t<decltype(SERIALIZER)>> SLANG_CONCAT( \ + _scopedSerializerStruct, \ + __LINE__)(SERIALIZER) -#define SLANG_SCOPED_SERIALIZER_VARIANT(SERIALIZER) \ - ::Slang::ScopedSerializerVariant SLANG_CONCAT(_scopedSerializerVariant, __LINE__)(SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_VARIANT(SERIALIZER) \ + ::Slang::ScopedSerializerVariant<std::remove_reference_t<decltype(SERIALIZER)>> SLANG_CONCAT( \ + _scopedSerializerVariant, \ + __LINE__)(SERIALIZER) -#define SLANG_SCOPED_SERIALIZER_TUPLE(SERIALIZER) \ - ::Slang::ScopedSerializerTuple SLANG_CONCAT(_scopedSerializerTuple, __LINE__)(SERIALIZER) +#define SLANG_SCOPED_SERIALIZER_TUPLE(SERIALIZER) \ + ::Slang::ScopedSerializerTuple<std::remove_reference_t<decltype(SERIALIZER)>> SLANG_CONCAT( \ + _scopedSerializerTuple, \ + __LINE__)(SERIALIZER) // // Containers like arrays and dictionaries are more // difficult to serialize than typical user-defined -// types for a few reasons: -// -// * They typically need to have distinct code paths -// for reading and writing, so they don't benefit -// much from having a unified read/write abstraction. -// -// * They need to be written as templates, to abstract -// over the element type, and thus need to be -// defined in headers. -// -// * Because the element type might require a more -// specialized type of serialization context, they -// also need to be templated on the type of the -// serializer itself. -// -// With all that said, the definitions themselves +// types because they typically need to have distinct +// code paths for reading and writing, so they don't +// benefit much from having a unified read/write abstraction. +/// +// That said, the definitions themselves // are fairly straightforward. All we have to do is // branch on whether we are reading or writing and // either iterate over the serialized data to fill @@ -856,7 +927,7 @@ void serialize(S const& serializer, KeyValuePair<K, V>& value) } template<typename S, typename K, typename V> -void serialize(S const& serializer, std::pair<K, V>& value) +SLANG_FORCE_INLINE void serialize(S const& serializer, std::pair<K, V>& value) { SLANG_SCOPED_SERIALIZER_TUPLE(serializer); serialize(serializer, value.first); @@ -864,7 +935,7 @@ void serialize(S const& serializer, std::pair<K, V>& value) } template<typename S, typename K, typename V> -void serialize(S const& serializer, Dictionary<K, V>& value) +SLANG_FORCE_INLINE void serialize(S const& serializer, Dictionary<K, V>& value) { SLANG_SCOPED_SERIALIZER_DICTIONARY(serializer); if (isWriting(serializer)) @@ -957,27 +1028,26 @@ void serialize(S const& serializer, OrderedDictionary<K, V>& value) // template<typename S, typename T> -void serializeObjectContents(S const& serializer, T* value, void*) +SLANG_FORCE_INLINE void serializeObjectContents(S const& serializer, T* value, void*) { serialize(serializer, *value); } -template<typename I, typename C, typename T> +template<typename S, typename T> void _serializeObjectContentsCallback(void* valuePtr, void* impl, void* context) { - Serializer_<I, C> serializer((I*)impl, (C*)context); + S serializer((typename S::Impl*)impl, (typename S::Context*)context); auto value = (T*)valuePtr; serializeObjectContents(serializer, value, (T*)nullptr); } -template<typename I, typename C, typename T> -void deferSerializeObjectContents(Serializer_<I, C> const& serializer, T* value) +template<typename S, typename T> +SLANG_FORCE_INLINE void deferSerializeObjectContents(S const& serializer, T* value) { - ((Serializer)serializer) - ->handleDeferredObjectContents( - value, - _serializeObjectContentsCallback<I, C, T>, - serializer.getContext()); + serializer->handleDeferredObjectContents( + value, + _serializeObjectContentsCallback<S, T>, + serializer.getContext()); } template<typename S, typename T> @@ -990,48 +1060,46 @@ void serializeObject(S const& serializer, T*& value, void*) deferSerializeObjectContents(serializer, value); } -template<typename I, typename C, typename T> +template<typename S, typename T> void _serializeObjectCallback(void* valuePtr, void* impl, void* context) { - Serializer_<I, C> serializer((I*)impl, (C*)context); + S serializer((typename S::Impl*)impl, (typename S::Context*)context); auto& value = *(T**)valuePtr; serializeObject(serializer, value, (T*)nullptr); } -template<typename I, typename C, typename T> -void serializeSharedPtr(Serializer_<I, C> const& serializer, T*& value) +template<typename S, typename T> +SLANG_FORCE_INLINE void serializeSharedPtr(S const& serializer, T*& value) { - ((Serializer)serializer) - ->handleSharedPtr( - *(void**)&value, - _serializeObjectCallback<I, C, T>, - serializer.getContext()); + serializer->handleSharedPtr( + *(void**)&value, + _serializeObjectCallback<S, T>, + serializer.getContext()); } -template<typename I, typename C, typename T> -void serializeUniquePtr(Serializer_<I, C> const& serializer, T*& value) +template<typename S, typename T> +SLANG_FORCE_INLINE void serializeUniquePtr(S const& serializer, T*& value) { - ((Serializer)serializer) - ->handleUniquePtr( - *(void**)&value, - _serializeObjectCallback<I, C, T>, - serializer.getContext()); + serializer->handleUniquePtr( + *(void**)&value, + _serializeObjectCallback<S, T>, + serializer.getContext()); } template<typename S, typename T> -void serializePtr(S const& serializer, T*& value, void*) +SLANG_FORCE_INLINE void serializePtr(S const& serializer, T*& value, void*) { serializeSharedPtr(serializer, value); } template<typename S, typename T> -void serialize(S const& serializer, T*& value) +SLANG_FORCE_INLINE void serialize(S const& serializer, T*& value) { serializePtr(serializer, value, (T*)nullptr); } template<typename S, typename T> -void serialize(S const& serializer, RefPtr<T>& value) +SLANG_FORCE_INLINE void serialize(S const& serializer, RefPtr<T>& value) { T* raw = value; serialize(serializer, raw); |
