Skip to content

Commit d2ebaff

Browse files
committed
feat(graphics): add stable mesh resource caching
1 parent 95a3265 commit d2ebaff

23 files changed

Lines changed: 900 additions & 58 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- `Engine/` builds the C++17 `PyramidEngine` library.
66
- Active modules are Core, Graphics, Math, Platform, and Utils.
77
- `Examples/BasicGame` and `Examples/BasicRendering` are the graphical references.
8-
- `Tests/PublicApiLinkage.cpp` protects selected public symbols; `Tests/WindowResizeEventTests.cpp` protects resize callback semantics; `Tests/CameraViewportTests.cpp` protects projection resizing; `Tests/FramebufferResizeTests.cpp` protects framebuffer specification and zero-area resize behavior; `Tests/TextureLoadingTests.cpp` protects transactional file loading and OpenGL upload state; `Tests/SceneTransformTests.cpp` protects hierarchy composition, invalidation, reparenting, cycle rejection, and parent-destruction behavior; `Tests/CameraFrustumTests.cpp` protects camera orientation, frustum extraction, transformed bounds, and linear/octree scene visibility; `Tests/OctreeUpdateTests.cpp` protects incremental movement, insertion, removal, and stable-scene synchronization; `Tests/OctreeQueryTests.cpp` protects bounds-accurate point, sphere, box, and nearest-first ray queries plus linear/octree parity; `Tests/NearestQueryTests.cpp` protects bounds-distance nearest/K-nearest ordering, limits, root-overflow behavior, and octree/linear parity. `Tests/OctreeConfigurationTests.cpp` protects atomic bounds/depth/capacity changes, tracked-object preservation, invalid configuration rejection, and constructor normalization. `Tests/OctreeCompactionTests.cpp` protects automatic branch collapse after removals and movement, explicit no-op compaction, synchronization compaction statistics, object preservation, and health-metric consistency. `Tests/RenderObjectBoundsTests.cpp` protects mesh-derived/manual transformed bounds. `Tests/MeshResourceTests.cpp` protects mesh validation, immutable metadata, topology-aware indexed/non-indexed submission, and `RenderObject` integration.
8+
- `Tests/PublicApiLinkage.cpp` protects selected public symbols; `Tests/WindowResizeEventTests.cpp` protects resize callback semantics; `Tests/CameraViewportTests.cpp` protects projection resizing; `Tests/FramebufferResizeTests.cpp` protects framebuffer specification and zero-area resize behavior; `Tests/TextureLoadingTests.cpp` protects transactional file loading and OpenGL upload state; `Tests/SceneTransformTests.cpp` protects hierarchy composition, invalidation, reparenting, cycle rejection, and parent-destruction behavior; `Tests/CameraFrustumTests.cpp` protects camera orientation, frustum extraction, transformed bounds, and linear/octree scene visibility; `Tests/OctreeUpdateTests.cpp` protects incremental movement, insertion, removal, and stable-scene synchronization; `Tests/OctreeQueryTests.cpp` protects bounds-accurate point, sphere, box, and nearest-first ray queries plus linear/octree parity; `Tests/NearestQueryTests.cpp` protects bounds-distance nearest/K-nearest ordering, limits, root-overflow behavior, and octree/linear parity. `Tests/OctreeConfigurationTests.cpp` protects atomic bounds/depth/capacity changes, tracked-object preservation, invalid configuration rejection, and constructor normalization. `Tests/OctreeCompactionTests.cpp` protects automatic branch collapse after removals and movement, explicit no-op compaction, synchronization compaction statistics, object preservation, and health-metric consistency. `Tests/RenderObjectBoundsTests.cpp` protects mesh-derived/manual transformed bounds. `Tests/MeshResourceTests.cpp` protects mesh validation, immutable metadata, topology-aware indexed/non-indexed submission, and `RenderObject` integration. `Tests/MeshCacheTests.cpp` protects deterministic identifiers, exact-content deduplication, alias conflicts, upload counts, strong residency, unused collection, explicit eviction, and external-owner lifetime.
99
- `Tests/Consumer` validates the installed CMake package.
1010
- `vendor/glad` is a bundled public dependency. libjpeg-turbo is an external open-source dependency resolved through CMake `FindJPEG`.
1111
- The supported Windows toolchain is MSYS2 UCRT64 with MinGW-w64 GCC; Clang is also validated. Visual Studio is not required.
@@ -32,7 +32,7 @@ ctest --preset test-gcc-release
3232

3333
- C++17, four spaces, braces on new lines.
3434
- Types/public methods use `PascalCase`; locals/parameters use `camelCase`; fields use `m_`.
35-
- Prefer RAII, explicit ownership, and `PYRAMID_LOG_*` diagnostics. Geometry passed to scenes must use `Mesh`; do not reintroduce raw vertex-array fields on `RenderObject`.
35+
- Prefer RAII, explicit ownership, and `PYRAMID_LOG_*` diagnostics. Geometry passed to scenes must use `Mesh`; do not reintroduce raw vertex-array fields on `RenderObject`. Shared reusable geometry should be acquired through a graphics-device-bound `MeshCache`; do not create parallel uploads for byte-identical mesh specifications.
3636
- Do not add required interface methods with silent no-op defaults.
3737
- Do not expose source-tree absolute paths through installed target interfaces.
3838

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to Pyramid Engine are documented here. The project is pre-al
44

55
## [Unreleased]
66

7+
### Stable mesh identifiers and resource caching
8+
9+
- Added deterministic 128-bit `MeshAssetId` values from caller-owned stable names or exact mesh content. Content fingerprints include vertex/index bytes, layout semantics, counts, normalization flags, and primitive topology while excluding debug names.
10+
- Added `MeshCache`, which owns one resident GPU mesh per unique content fingerprint and resolves any number of stable asset-ID aliases to that upload.
11+
- Added hard conflict detection when one explicit asset identifier is reused for different resident geometry.
12+
- Added explicit `Evict()`, `CollectUnused()`, and `Clear()` lifetime controls; cache eviction releases cache ownership without invalidating external `shared_ptr<Mesh>` instances.
13+
- Added mesh/cache residency statistics for hits, misses, creations, failures, conflicts, evictions, identifier aliases, externally referenced resources, and resident geometry bytes.
14+
- Migrated both examples to the cache and added `Graphics.MeshCache` coverage for deterministic IDs, alias deduplication, upload counts, conflicts, residency, collection, eviction, and external-owner lifetime.
15+
716
### Engine-owned mesh resources
817

918
- Added `Mesh` and `MeshSpecification` as the authoritative geometry resource for vertex/index ownership, layout, draw count, primitive topology, and immutable local bounds.

Engine/Graphics/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ target_sources(PyramidEngine
1414
source/Buffer/UniformBuffer.cpp
1515
source/Geometry/MeshBounds.cpp
1616
source/Geometry/Mesh.cpp
17+
source/Geometry/MeshCache.cpp
1718
source/OpenGL/Shader/OpenGLShader.cpp
1819
source/Texture.cpp
1920
source/OpenGL/OpenGLTexture.cpp
@@ -43,6 +44,7 @@ target_sources(PyramidEngine
4344
include/Pyramid/Graphics/Geometry/Vertex.hpp
4445
include/Pyramid/Graphics/Geometry/MeshBounds.hpp
4546
include/Pyramid/Graphics/Geometry/Mesh.hpp
47+
include/Pyramid/Graphics/Geometry/MeshCache.hpp
4648
include/Pyramid/Graphics/PrimitiveTopology.hpp
4749
include/Pyramid/Graphics/Renderer/RenderSystem.hpp
4850
include/Pyramid/Graphics/Renderer/ShaderPathResolver.hpp

Engine/Graphics/include/Pyramid/Graphics/Geometry/Mesh.hpp

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#include <Pyramid/Graphics/PrimitiveTopology.hpp>
66
#include <Pyramid/Math/Math.hpp>
77

8+
#include <cstddef>
89
#include <memory>
910
#include <string>
11+
#include <string_view>
1012

1113
namespace Pyramid
1214
{
@@ -20,12 +22,46 @@ namespace Pyramid
2022
class CommandBuffer;
2123
}
2224

25+
/**
26+
* @brief Stable 128-bit identifier for a mesh asset.
27+
*
28+
* Caller-defined identifiers can be created from stable names such as asset
29+
* paths. When a specification does not provide an identifier, Pyramid derives
30+
* one deterministically from the exact vertex/index bytes, layout, counts, and
31+
* primitive topology. Debug names are deliberately excluded.
32+
*/
33+
struct MeshAssetId
34+
{
35+
u64 high = 0;
36+
u64 low = 0;
37+
38+
bool IsValid() const { return high != 0 || low != 0; }
39+
std::string ToString() const;
40+
41+
static MeshAssetId FromString(std::string_view stableName);
42+
43+
bool operator==(const MeshAssetId& other) const
44+
{
45+
return high == other.high && low == other.low;
46+
}
47+
48+
bool operator!=(const MeshAssetId& other) const
49+
{
50+
return !(*this == other);
51+
}
52+
};
53+
54+
struct MeshAssetIdHash
55+
{
56+
std::size_t operator()(const MeshAssetId& identifier) const noexcept;
57+
};
58+
2359
/**
2460
* @brief Immutable source description used to create a GPU-backed mesh.
2561
*
2662
* Pyramid copies the supplied vertex/index data into engine-owned GPU buffers.
2763
* The caller only needs to keep the pointed-to memory alive for the duration of
28-
* Mesh::Create().
64+
* Mesh::Create() or MeshCache::GetOrCreate().
2965
*/
3066
struct MeshSpecification
3167
{
@@ -39,14 +75,21 @@ namespace Pyramid
3975

4076
PrimitiveTopology topology = PrimitiveTopology::Triangles;
4177
std::string name;
78+
79+
/**
80+
* Optional stable caller-owned identifier. If invalid, the content
81+
* fingerprint becomes the asset identifier automatically.
82+
*/
83+
MeshAssetId assetId;
4284
};
4385

4486
/**
4587
* @brief Engine-owned immutable geometry resource.
4688
*
4789
* A mesh owns the vertex array plus its vertex/index buffers and keeps the
48-
* validated layout, draw count, topology, and local bounds together. This
49-
* prevents RenderObject from depending on mutable raw vertex-array state.
90+
* validated layout, draw count, topology, identifiers, and local bounds
91+
* together. This prevents RenderObject from depending on mutable raw
92+
* vertex-array state.
5093
*/
5194
class Mesh final
5295
{
@@ -55,6 +98,12 @@ namespace Pyramid
5598
IGraphicsDevice& device,
5699
const MeshSpecification& specification);
57100

101+
/**
102+
* @brief Calculate the deterministic fingerprint for a valid specification.
103+
* @return Invalid identifier when the specification is malformed.
104+
*/
105+
static MeshAssetId CalculateContentId(const MeshSpecification& specification);
106+
58107
Mesh(const Mesh&) = delete;
59108
Mesh& operator=(const Mesh&) = delete;
60109
Mesh(Mesh&&) = delete;
@@ -69,9 +118,20 @@ namespace Pyramid
69118
u32 GetVertexCount() const { return m_vertexCount; }
70119
u32 GetIndexCount() const { return m_indexCount; }
71120
u32 GetDrawCount() const { return IsIndexed() ? m_indexCount : m_vertexCount; }
121+
u64 GetVertexDataSize() const { return m_vertexDataSize; }
122+
u64 GetIndexDataSize() const
123+
{
124+
return static_cast<u64>(m_indexCount) * sizeof(u32);
125+
}
126+
u64 GetGeometryDataSize() const
127+
{
128+
return GetVertexDataSize() + GetIndexDataSize();
129+
}
72130
PrimitiveTopology GetTopology() const { return m_topology; }
73131
const BufferLayout& GetLayout() const { return m_layout; }
74132
const std::string& GetName() const { return m_name; }
133+
MeshAssetId GetAssetId() const { return m_assetId; }
134+
MeshAssetId GetContentId() const { return m_contentId; }
75135

76136
void GetLocalBounds(Math::Vec3& minPoint, Math::Vec3& maxPoint) const;
77137

@@ -85,9 +145,12 @@ namespace Pyramid
85145
std::shared_ptr<IVertexBuffer> vertexBuffer,
86146
std::shared_ptr<IIndexBuffer> indexBuffer,
87147
BufferLayout layout,
148+
u32 vertexDataSize,
88149
u32 vertexCount,
89150
u32 indexCount,
90151
PrimitiveTopology topology,
152+
MeshAssetId assetId,
153+
MeshAssetId contentId,
91154
const Math::Vec3& localBoundsMin,
92155
const Math::Vec3& localBoundsMax,
93156
std::string name);
@@ -96,9 +159,12 @@ namespace Pyramid
96159
std::shared_ptr<IVertexBuffer> m_vertexBuffer;
97160
std::shared_ptr<IIndexBuffer> m_indexBuffer;
98161
BufferLayout m_layout;
162+
u32 m_vertexDataSize = 0;
99163
u32 m_vertexCount = 0;
100164
u32 m_indexCount = 0;
101165
PrimitiveTopology m_topology = PrimitiveTopology::Triangles;
166+
MeshAssetId m_assetId;
167+
MeshAssetId m_contentId;
102168
Math::Vec3 m_localBoundsMin = Math::Vec3(-0.5f);
103169
Math::Vec3 m_localBoundsMax = Math::Vec3(0.5f);
104170
std::string m_name;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#pragma once
2+
3+
#include <Pyramid/Core/Prerequisites.hpp>
4+
#include <Pyramid/Graphics/Geometry/Mesh.hpp>
5+
6+
#include <memory>
7+
#include <unordered_map>
8+
9+
namespace Pyramid
10+
{
11+
class IGraphicsDevice;
12+
13+
/**
14+
* @brief Snapshot of mesh-cache residency and acquisition activity.
15+
*/
16+
struct MeshCacheStats
17+
{
18+
u64 cacheHits = 0;
19+
u64 cacheMisses = 0;
20+
u64 meshesCreated = 0;
21+
u64 creationFailures = 0;
22+
u64 identifierConflicts = 0;
23+
u64 evictions = 0;
24+
25+
u32 residentMeshes = 0;
26+
u32 residentAssetIds = 0;
27+
u32 externallyReferencedMeshes = 0;
28+
u64 residentVertexBytes = 0;
29+
u64 residentIndexBytes = 0;
30+
31+
u64 GetResidentGeometryBytes() const
32+
{
33+
return residentVertexBytes + residentIndexBytes;
34+
}
35+
};
36+
37+
/**
38+
* @brief Graphics-device-bound cache for immutable Mesh resources.
39+
*
40+
* Exact content is uploaded once even when it is requested through several
41+
* stable asset identifiers. The cache owns one strong reference per unique
42+
* geometry resource, so a resident mesh remains alive until it is explicitly
43+
* evicted, cleared, or removed by CollectUnused(). Existing external
44+
* shared_ptr instances remain valid after cache eviction.
45+
*
46+
* The cache and its meshes must be destroyed before the graphics
47+
* device/context. MeshCache is intended for use on the graphics thread and is
48+
* not internally synchronized.
49+
*/
50+
class MeshCache final
51+
{
52+
public:
53+
explicit MeshCache(IGraphicsDevice& device);
54+
55+
MeshCache(const MeshCache&) = delete;
56+
MeshCache& operator=(const MeshCache&) = delete;
57+
MeshCache(MeshCache&&) = delete;
58+
MeshCache& operator=(MeshCache&&) = delete;
59+
~MeshCache() = default;
60+
61+
/**
62+
* @brief Return a resident mesh or upload it once when absent.
63+
*
64+
* An invalid specification returns nullptr. Reusing one explicit asset ID
65+
* with different geometry is rejected while the original entry is resident.
66+
*/
67+
std::shared_ptr<Mesh> GetOrCreate(const MeshSpecification& specification);
68+
69+
std::shared_ptr<Mesh> Find(MeshAssetId assetId) const;
70+
bool Contains(MeshAssetId assetId) const;
71+
72+
/**
73+
* @brief Evict the resolved mesh and all identifiers that alias it.
74+
* @return true when a resident resource was removed.
75+
*/
76+
bool Evict(MeshAssetId assetId);
77+
78+
/**
79+
* @brief Evict resources only owned by the cache.
80+
* @return number of unique meshes removed.
81+
*/
82+
u32 CollectUnused();
83+
84+
/**
85+
* @brief Evict every resident resource without invalidating external owners.
86+
* @return number of unique meshes removed.
87+
*/
88+
u32 Clear();
89+
90+
u32 GetResidentCount() const { return static_cast<u32>(m_residents.size()); }
91+
MeshCacheStats GetStats() const;
92+
93+
private:
94+
struct Resident
95+
{
96+
std::shared_ptr<Mesh> mesh;
97+
};
98+
99+
void RemoveAliasesForContent(MeshAssetId contentId);
100+
101+
IGraphicsDevice* m_device = nullptr;
102+
std::unordered_map<MeshAssetId, MeshAssetId, MeshAssetIdHash> m_assetToContent;
103+
std::unordered_map<MeshAssetId, Resident, MeshAssetIdHash> m_residents;
104+
105+
u64 m_cacheHits = 0;
106+
u64 m_cacheMisses = 0;
107+
u64 m_meshesCreated = 0;
108+
u64 m_creationFailures = 0;
109+
u64 m_identifierConflicts = 0;
110+
u64 m_evictions = 0;
111+
};
112+
}

0 commit comments

Comments
 (0)