From 893f192eaaf082cfa92486ac4db4cd997d9e1f8e Mon Sep 17 00:00:00 2001 From: Vladislav Belov Date: Sun, 12 Oct 2025 00:53:12 +0200 Subject: [PATCH] Makes TextRenderer allocates using LinearAllocator Also replaces DynamicArena for model and terrain rendering by LinearAllocator. --- source/graphics/TextRenderer.cpp | 20 ++++++------ source/graphics/TextRenderer.h | 12 ++++++-- source/renderer/DecalRData.cpp | 10 +++--- source/renderer/ModelRenderer.cpp | 39 ++++++++++++----------- source/renderer/PatchRData.cpp | 51 +++++++++++++++---------------- source/renderer/Renderer.cpp | 33 +++++++++++++++++--- source/renderer/Renderer.h | 7 +++++ 7 files changed, 102 insertions(+), 70 deletions(-) diff --git a/source/graphics/TextRenderer.cpp b/source/graphics/TextRenderer.cpp index 6d623f2248..7ddbcecd71 100644 --- a/source/graphics/TextRenderer.cpp +++ b/source/graphics/TextRenderer.cpp @@ -52,6 +52,7 @@ constexpr size_t MAX_CHAR_COUNT_PER_BATCH = 65536 / 4; } // anonymous namespace CTextRenderer::CTextRenderer() + : m_ScopedLinearAllocator{g_Renderer.GetLinearAllocator()}, m_Batches(m_ScopedLinearAllocator) { ResetTranslate(); SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f)); @@ -174,7 +175,7 @@ void CTextRenderer::PutString(float x, float y, const std::wstring* buf, bool ow // If any state has changed since the last batch, start a new batch if (m_Dirty) { - SBatch batch; + SBatch batch{m_ScopedLinearAllocator}; batch.chars = 0; batch.translate = m_Translate; batch.color = m_Color; @@ -210,16 +211,16 @@ void CTextRenderer::Render( const CVector2D& transformScale, const CVector2D& translation, const bool debugFontBox, const CColor& debugBoxColor) { - std::vector indices; - std::vector positions; - std::vector uvs; + std::vector> indices{m_ScopedLinearAllocator}; + std::vector> positions{m_ScopedLinearAllocator}; + std::vector> uvs{m_ScopedLinearAllocator}; // Try to merge non-consecutive batches that share the same font/color/translate: // sort the batch list by font, then merge the runs of adjacent compatible batches m_Batches.sort(SBatchCompare()); - for (std::list::iterator it = m_Batches.begin(); it != m_Batches.end(); ) + for (SBatchList::iterator it = m_Batches.begin(); it != m_Batches.end(); ) { - std::list::iterator next = std::next(it); + SBatchList::iterator next = std::next(it); if (next != m_Batches.end() && it->chars + next->chars <= MAX_CHAR_COUNT_PER_BATCH && it->font == next->font && it->color == next->color && it->translate == next->translate) { it->chars += next->chars; @@ -238,10 +239,8 @@ void CTextRenderer::Render( bool translationChanged = false; CTexture* lastTexture = nullptr; - for (std::list::iterator it = m_Batches.begin(); it != m_Batches.end(); ++it) + for (SBatch& batch : m_Batches) { - SBatch& batch = *it; - if (lastTexture != batch.font->GetTexture().get()) { batch.font->InitalizeAtlasTextureIfNeeded(deviceCommandContext); @@ -294,9 +293,8 @@ void CTextRenderer::Render( idx = 0; }; - for (std::list::iterator runit = batch.runs.begin(); runit != batch.runs.end(); ++runit) + for (SBatchRun& run : batch.runs) { - SBatchRun& run = *runit; float x{std::ceil(run.x)}; float y{std::ceil(run.y)}; for (size_t i = 0; i < run.text->size(); ++i) diff --git a/source/graphics/TextRenderer.h b/source/graphics/TextRenderer.h index e933967be7..2846644e29 100644 --- a/source/graphics/TextRenderer.h +++ b/source/graphics/TextRenderer.h @@ -19,9 +19,11 @@ #define INCLUDED_TEXTRENDERER #include "graphics/Color.h" +#include "lib/allocators/STLAllocators.h" #include "maths/Rect.h" #include "maths/Vector2D.h" #include "ps/CStrIntern.h" +#include "ps/memory/LinearAllocator.h" #include #include @@ -159,11 +161,16 @@ private: CVector2D translate; CColor color; CFont* font; - std::list runs; + using SBatchRunList = std::list>; + SBatchRunList runs; + + SBatch(PS::Memory::ScopedLinearAllocator& allocator) : runs{allocator} {} }; void PutString(float x, float y, const std::wstring* buf, bool owned); + PS::Memory::ScopedLinearAllocator m_ScopedLinearAllocator; + CVector2D m_Translate; CRect m_Clipping; @@ -173,7 +180,8 @@ private: bool m_Dirty = true; - std::list m_Batches; + using SBatchList = std::list>; + SBatchList m_Batches; }; #endif // INCLUDED_TEXTRENDERER diff --git a/source/renderer/DecalRData.cpp b/source/renderer/DecalRData.cpp index 8d56ed00dd..bd3f54bbe0 100644 --- a/source/renderer/DecalRData.cpp +++ b/source/renderer/DecalRData.cpp @@ -29,7 +29,6 @@ #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "lib/alignment.h" -#include "lib/allocators/DynamicArena.h" #include "lib/allocators/STLAllocators.h" #include "lib/debug.h" #include "lib/posix/posix_types.h" @@ -38,6 +37,7 @@ #include "ps/CLogger.h" #include "ps/CStrIntern.h" #include "ps/CStrInternStatic.h" +#include "ps/memory/LinearAllocator.h" #include "ps/Profile.h" #include "renderer/Renderer.h" #include "renderer/TerrainRenderer.h" @@ -143,12 +143,10 @@ void CDecalRData::RenderDecals( PROFILE3("render terrain decals"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain decals"); - using Arena = Allocators::DynamicArena<256 * KiB>; + PS::Memory::ScopedLinearAllocator scopedLinearAllocator{g_Renderer.GetLinearAllocator()}; - Arena arena; - - using Batches = std::vector>; - Batches batches((Batches::allocator_type(arena))); + using Batches = std::vector>; + Batches batches((Batches::allocator_type(scopedLinearAllocator))); batches.reserve(decals.size()); CShaderDefines contextDecal = context; diff --git a/source/renderer/ModelRenderer.cpp b/source/renderer/ModelRenderer.cpp index 4a0819a45e..c08e04937b 100644 --- a/source/renderer/ModelRenderer.cpp +++ b/source/renderer/ModelRenderer.cpp @@ -31,7 +31,6 @@ #include "graphics/Texture.h" #include "graphics/TextureManager.h" #include "lib/alignment.h" -#include "lib/allocators/DynamicArena.h" #include "lib/allocators/STLAllocators.h" #include "lib/debug.h" #include "lib/hash.h" @@ -41,6 +40,7 @@ #include "ps/CLogger.h" #include "ps/CStrIntern.h" #include "ps/CStrInternStatic.h" +#include "ps/memory/LinearAllocator.h" #include "ps/Profile.h" #include "renderer/MikktspaceWrap.h" #include "renderer/ModelRenderer.h" @@ -441,10 +441,9 @@ void ShaderModelRenderer::Render( * list in each, rebinding the GL state whenever it changes. */ - using Arena = Allocators::DynamicArena<256 * KiB>; + PS::Memory::ScopedLinearAllocator scopedLinearAllocator{g_Renderer.GetLinearAllocator()}; - Arena arena; - using ModelListAllocator = ProxyAllocator; + using ModelListAllocator = ProxyAllocator; using ModelList_t = std::vector; using MaterialBuckets_t = std::unordered_map< SMRMaterialBucketKey, @@ -453,9 +452,9 @@ void ShaderModelRenderer::Render( std::equal_to, ProxyAllocator< std::pair, - Arena> >; + PS::Memory::ScopedLinearAllocator>>; - MaterialBuckets_t materialBuckets((MaterialBuckets_t::allocator_type(arena))); + MaterialBuckets_t materialBuckets((MaterialBuckets_t::allocator_type(scopedLinearAllocator))); { PROFILE3("bucketing by material"); @@ -470,7 +469,7 @@ void ShaderModelRenderer::Render( if (it == materialBuckets.end()) { std::pair inserted = materialBuckets.insert( - std::make_pair(key, ModelList_t(ModelList_t::allocator_type(arena)))); + std::make_pair(key, ModelList_t(ModelList_t::allocator_type(scopedLinearAllocator)))); inserted.first->second.reserve(32); inserted.first->second.push_back(model); } @@ -481,19 +480,19 @@ void ShaderModelRenderer::Render( } } - using SortByDistItemsAllocator = ProxyAllocator; - std::vector sortByDistItems((SortByDistItemsAllocator(arena))); + using SortByDistItemsAllocator = ProxyAllocator; + std::vector sortByDistItems((SortByDistItemsAllocator(scopedLinearAllocator))); - using SortByTechItemsAllocator = ProxyAllocator; - std::vector sortByDistTechs((SortByTechItemsAllocator(arena))); + using SortByTechItemsAllocator = ProxyAllocator; + std::vector sortByDistTechs((SortByTechItemsAllocator(scopedLinearAllocator))); // indexed by sortByDistItems[i].techIdx // (which stores indexes instead of CShaderTechniquePtr directly // to avoid the shared_ptr copy cost when sorting; maybe it'd be better // if we just stored raw CShaderTechnique* and assumed the shader manager // will keep it alive long enough) - using TechBucketsAllocator = ProxyAllocator; - std::vector techBuckets((TechBucketsAllocator(arena))); + using TechBucketsAllocator = ProxyAllocator; + std::vector techBuckets((TechBucketsAllocator(scopedLinearAllocator))); { PROFILE3("processing material buckets"); @@ -555,7 +554,7 @@ void ShaderModelRenderer::Render( // (This exists primarily because techBuckets wants a CModel**; // we could avoid the cost of copying into this list by adding // a stride length into techBuckets and not requiring contiguous CModel*s) - std::vector sortByDistModels((ModelListAllocator(arena))); + std::vector sortByDistModels((ModelListAllocator(scopedLinearAllocator))); if (!sortByDistItems.empty()) { @@ -608,19 +607,19 @@ void ShaderModelRenderer::Render( // This vector keeps track of texture changes during rendering. It is kept outside the // loops to avoid excessive reallocations. The token allocation of 64 elements // should be plenty, though it is reallocated below (at a cost) if necessary. - using TextureListAllocator = ProxyAllocator; - std::vector currentTexs((TextureListAllocator(arena))); + using TextureListAllocator = ProxyAllocator; + std::vector currentTexs((TextureListAllocator(scopedLinearAllocator))); currentTexs.reserve(64); // texBindings holds the identifier bindings in the shader, which can no longer be defined // statically in the ShaderRenderModifier class. texBindingNames uses interned strings to // keep track of when bindings need to be reevaluated. - using BindingListAllocator = ProxyAllocator; - std::vector texBindings((BindingListAllocator(arena))); + using BindingListAllocator = ProxyAllocator; + std::vector texBindings((BindingListAllocator(scopedLinearAllocator))); texBindings.reserve(64); - using BindingNamesListAllocator = ProxyAllocator; - std::vector texBindingNames((BindingNamesListAllocator(arena))); + using BindingNamesListAllocator = ProxyAllocator; + std::vector texBindingNames((BindingNamesListAllocator(scopedLinearAllocator))); texBindingNames.reserve(64); while (idxTechStart < techBuckets.size()) diff --git a/source/renderer/PatchRData.cpp b/source/renderer/PatchRData.cpp index 579b56d58b..a37cf5ceda 100644 --- a/source/renderer/PatchRData.cpp +++ b/source/renderer/PatchRData.cpp @@ -35,7 +35,6 @@ #include "graphics/TextRenderer.h" #include "graphics/TextureManager.h" #include "lib/alignment.h" -#include "lib/allocators/DynamicArena.h" #include "lib/allocators/STLAllocators.h" #include "lib/code_generation.h" #include "lib/debug.h" @@ -45,6 +44,7 @@ #include "ps/CStrIntern.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" +#include "ps/memory/LinearAllocator.h" #include "ps/Profile.h" #include "renderer/AlphaMapCalculator.h" #include "renderer/BlendShapes.h" @@ -841,16 +841,14 @@ void CPatchRData::Update(CSimulation2* simulation) // batches uses a arena allocator. (All allocations are short-lived so we can // just throw away the whole arena at the end of each frame.) -using Arena = Allocators::DynamicArena<1 * MiB>; - // std::map types with appropriate arena allocators and default comparison operator template -using PooledBatchMap = std::map, ProxyAllocator, Arena>>; +using PooledBatchMap = std::map, ProxyAllocator, PS::Memory::ScopedLinearAllocator>>; // Equivalent to "m[k]", when it returns a arena-allocated std::map (since we can't // use the default constructor in that case) template -typename M::mapped_type& PooledMapGet(M& m, const typename M::key_type& k, Arena& arena) +typename M::mapped_type& PooledMapGet(M& m, const typename M::key_type& k, PS::Memory::ScopedLinearAllocator& arena) { return m.insert(std::make_pair(k, typename M::mapped_type(typename M::mapped_type::key_compare(), typename M::mapped_type::allocator_type(arena)) @@ -859,7 +857,7 @@ typename M::mapped_type& PooledMapGet(M& m, const typename M::key_type& k, Arena // Equivalent to "m[k]", when it returns a std::pair of arena-allocated std::vectors template -typename M::mapped_type& PooledPairGet(M& m, const typename M::key_type& k, Arena& arena) +typename M::mapped_type& PooledPairGet(M& m, const typename M::key_type& k, PS::Memory::ScopedLinearAllocator& arena) { return m.insert(std::make_pair(k, std::make_pair( typename M::mapped_type::first_type(typename M::mapped_type::first_type::allocator_type(arena)), @@ -868,7 +866,7 @@ typename M::mapped_type& PooledPairGet(M& m, const typename M::key_type& k, Aren } // Each multidraw batch has a list of index counts, and a list of pointers-to-first-indexes -using BatchElements = std::pair>, std::vector>>; +using BatchElements = std::pair>, std::vector>>; // Group batches by index buffer using IndexBufferBatches = PooledBatchMap; @@ -890,9 +888,9 @@ void CPatchRData::RenderBases( PROFILE3("render terrain bases"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain bases"); - Arena arena; + PS::Memory::ScopedLinearAllocator scopedLinearAllocator{g_Renderer.GetLinearAllocator()}; - ShaderTechniqueBatches batches(ShaderTechniqueBatches::key_compare(), (ShaderTechniqueBatches::allocator_type(arena))); + ShaderTechniqueBatches batches(ShaderTechniqueBatches::key_compare(), (ShaderTechniqueBatches::allocator_type(scopedLinearAllocator))); PROFILE_START("compute batches"); @@ -913,12 +911,12 @@ void CPatchRData::RenderBases( BatchElements& batch = PooledPairGet( PooledMapGet( PooledMapGet( - PooledMapGet(batches, std::make_pair(material.GetShaderEffect(), material.GetShaderDefines()), arena), - splat.m_Texture, arena + PooledMapGet(batches, std::make_pair(material.GetShaderEffect(), material.GetShaderDefines()), scopedLinearAllocator), + splat.m_Texture, scopedLinearAllocator ), - patch->m_VBBase->m_Owner, arena + patch->m_VBBase->m_Owner, scopedLinearAllocator ), - patch->m_VBBaseIndices->m_Owner, arena + patch->m_VBBaseIndices->m_Owner, scopedLinearAllocator ); batch.first.push_back(splat.m_IndexCount); @@ -1015,8 +1013,8 @@ void CPatchRData::RenderBases( */ struct SBlendBatch { - SBlendBatch(Arena& arena) : - m_Batches(VertexBufferBatches::key_compare(), VertexBufferBatches::allocator_type(arena)) + SBlendBatch(PS::Memory::ScopedLinearAllocator& allocator) : + m_Batches(VertexBufferBatches::key_compare(), VertexBufferBatches::allocator_type(allocator)) { } @@ -1031,12 +1029,12 @@ struct SBlendBatch struct SBlendStackItem { SBlendStackItem(CVertexBuffer::VBChunk* v, CVertexBuffer::VBChunk* i, - const std::vector& s, Arena& arena) : - vertices(v), indices(i), splats(s.begin(), s.end(), SplatStack::allocator_type(arena)) + const std::vector& s, PS::Memory::ScopedLinearAllocator& allocator) : + vertices(v), indices(i), splats(s.begin(), s.end(), SplatStack::allocator_type(allocator)) { } - using SplatStack = std::vector>; + using SplatStack = std::vector>; CVertexBuffer::VBChunk* vertices; CVertexBuffer::VBChunk* indices; SplatStack splats; @@ -1050,10 +1048,10 @@ void CPatchRData::RenderBlends( PROFILE3("render terrain blends"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain blends"); - Arena arena; + PS::Memory::ScopedLinearAllocator scopedLinearAllocator{g_Renderer.GetLinearAllocator()}; - using BatchesStack = std::vector>; - BatchesStack batches((BatchesStack::allocator_type(arena))); + using BatchesStack = std::vector>; + BatchesStack batches((BatchesStack::allocator_type(scopedLinearAllocator))); CShaderDefines contextBlend = context; contextBlend.Add(str_BLEND, str_1); @@ -1064,8 +1062,8 @@ void CPatchRData::RenderBlends( // to avoid heavy reallocations batches.reserve(256); - using BlendStacks = std::vector>; - BlendStacks blendStacks((BlendStacks::allocator_type(arena))); + using BlendStacks = std::vector>; + BlendStacks blendStacks((BlendStacks::allocator_type(scopedLinearAllocator))); blendStacks.reserve(patches.size()); // Extract all the blend splats from each patch @@ -1074,8 +1072,7 @@ void CPatchRData::RenderBlends( CPatchRData* patch = patches[i]; if (!patch->m_BlendSplats.empty()) { - - blendStacks.push_back(SBlendStackItem(patch->m_VBBlends.Get(), patch->m_VBBlendIndices.Get(), patch->m_BlendSplats, arena)); + blendStacks.push_back(SBlendStackItem(patch->m_VBBlends.Get(), patch->m_VBBlendIndices.Get(), patch->m_BlendSplats, scopedLinearAllocator)); // Reverse the splats so the first to be rendered is at the back of the list std::reverse(blendStacks.back().splats.begin(), blendStacks.back().splats.end()); } @@ -1100,7 +1097,7 @@ void CPatchRData::RenderBlends( CVertexBuffer::VBChunk* vertices = blendStacks[k].vertices; CVertexBuffer::VBChunk* indices = blendStacks[k].indices; - BatchElements& batch = PooledPairGet(PooledMapGet(batches.back().m_Batches, vertices->m_Owner, arena), indices->m_Owner, arena); + BatchElements& batch = PooledPairGet(PooledMapGet(batches.back().m_Batches, vertices->m_Owner, scopedLinearAllocator), indices->m_Owner, scopedLinearAllocator); batch.first.push_back(splats.back().m_IndexCount); batch.second.push_back(indices->m_Index + splats.back().m_IndexStart); @@ -1126,7 +1123,7 @@ void CPatchRData::RenderBlends( if (bestStackSize == 0) break; - SBlendBatch layer(arena); + SBlendBatch layer(scopedLinearAllocator); layer.m_Texture = bestTex; if (!bestTex->GetMaterial().GetSamplers().empty()) { diff --git a/source/renderer/Renderer.cpp b/source/renderer/Renderer.cpp index 0ab581a3cd..fd1e2a97ce 100644 --- a/source/renderer/Renderer.cpp +++ b/source/renderer/Renderer.cpp @@ -53,6 +53,7 @@ #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" +#include "ps/memory/LinearAllocator.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" @@ -99,7 +100,7 @@ class CRendererStatsTable : public AbstractProfileTable { NONCOPYABLE(CRendererStatsTable); public: - CRendererStatsTable(const CRenderer::Stats& st); + CRendererStatsTable(const CRenderer::Stats& st, const PS::Memory::LinearAllocator& linearAllocator); // Implementation of AbstractProfileTable interface CStr GetName() override; @@ -112,6 +113,7 @@ public: private: /// Reference to the renderer singleton's stats const CRenderer::Stats& Stats; + const PS::Memory::LinearAllocator& m_LinearAllocator; /// Column descriptions std::vector columnDescriptions; @@ -129,6 +131,7 @@ private: Row_VBAllocated, Row_TextureMemory, Row_ShadersLoaded, + Row_LinearAllocator, // Must be last to count number of rows NumberRows @@ -136,8 +139,8 @@ private: }; // Construction -CRendererStatsTable::CRendererStatsTable(const CRenderer::Stats& st) - : Stats(st) +CRendererStatsTable::CRendererStatsTable(const CRenderer::Stats& st, const PS::Memory::LinearAllocator& linearAllocator) + : Stats(st), m_LinearAllocator(linearAllocator) { columnDescriptions.push_back(ProfileColumn("Name", 230)); columnDescriptions.push_back(ProfileColumn("Value", 100)); @@ -236,6 +239,12 @@ CStr CRendererStatsTable::GetCellText(size_t row, size_t col) sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)g_Renderer.GetShaderManager().GetNumEffectsLoaded()); return buf; + case Row_LinearAllocator: + if (col == 0) + return "linear allocator"; + sprintf_s(buf, sizeof(buf), "%lu", static_cast(m_LinearAllocator.GetCapacity())); + return buf; + default: return "???"; } @@ -292,6 +301,13 @@ public: CFontManager fontManager; + // During rendering we need to collect and sort many objects. To reduce + // the allocation cost and increase cache locality we use the + // LinearAllocator. + // If we need to have more than 16MiB of continious memory then we're doing + // a lot of unnecessary work. + PS::Memory::LinearAllocator linearAllocator{1 * MiB, 16 * MiB}; + struct VertexAttributesHash { size_t operator()(const std::vector& attributes) const; @@ -304,7 +320,7 @@ public: Internals(Renderer::Backend::IDevice* device) : device(device), deviceCommandContext(device->CreateCommandContext()), - IsOpen(false), ShadersDirty(true), profileTable(g_Renderer.m_Stats), + IsOpen(false), ShadersDirty(true), profileTable(g_Renderer.m_Stats, linearAllocator), shaderManager(device), textureManager(g_VFS, false, device), vertexBufferManager(device), postprocManager(device), sceneRenderer(device) { @@ -623,6 +639,8 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger) PROFILE2_ATTR("particles: %zu", stats.m_Particles); g_Profiler2.RecordGPUFrameEnd(m->deviceCommandContext.get()); + + m->linearAllocator.Release(); } void CRenderer::RenderFrame2D(const bool renderGUI, const bool renderLogger) @@ -858,6 +876,8 @@ void CRenderer::EndFrame() PROFILE3("end frame"); m->sceneRenderer.EndFrame(); + + m->linearAllocator.Release(); } void CRenderer::MakeShadersDirty() @@ -930,3 +950,8 @@ Renderer::Backend::IVertexInputLayout* CRenderer::GetVertexInputLayout( it->second = m->device->CreateVertexInputLayout(attributes); return it->second.get(); } + +PS::Memory::LinearAllocator& CRenderer::GetLinearAllocator() +{ + return m->linearAllocator; +} diff --git a/source/renderer/Renderer.h b/source/renderer/Renderer.h index 2bd07945f9..f218c0f8e7 100644 --- a/source/renderer/Renderer.h +++ b/source/renderer/Renderer.h @@ -33,6 +33,7 @@ class CShaderManager; class CTextureManager; class CTimeManager; class CVertexBufferManager; +namespace PS::Memory { class LinearAllocator; } namespace Renderer::Backend { class IDevice; } namespace Renderer::Backend { class IDeviceCommandContext; } namespace Renderer::Backend { class IVertexInputLayout; } @@ -144,6 +145,12 @@ public: Renderer::Backend::IVertexInputLayout* GetVertexInputLayout( const std::span attributes); + /** + * Currently using the linear allocated is allowed in small scopes to avoid + * high memory overhead. To validate that use PS::Memory::ScopedLinearAllocator. + */ + PS::Memory::LinearAllocator& GetLinearAllocator(); + protected: friend class CDecalRData; friend class CPatchRData;