graphics: add strong 64-bit resource name hashes

Introduce ResourceNameHash using wyhash for fast drawable and font
  secondary indexes without changing the global String::HashType.

  Replace the 32-bit resource indexes with direct 64-bit weak-handle
  lookups while keeping complete ResourceKey lookup authoritative.
  Preserve legacy persisted drawable hash support through a documented
  compatibility scan and migrate internal font operations to semantic
  name lookup.

  Add regression coverage proving names that collide under the legacy
  hash remain independently addressable.
This commit is contained in:
Martín Lucas Golini
2026-07-24 23:33:16 -03:00
parent 605c81087d
commit 8bdee0281f
9 changed files with 175 additions and 54 deletions

View File

@@ -4,6 +4,7 @@
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <eepp/core.hpp>
@@ -28,6 +29,42 @@ class ResourceId {
Uint64 mValue{ 0 };
};
/**
* Strong 64-bit hash of a semantic resource name.
*
* This is a fast, process-local convenience key for trusted resource names. It is deliberately
* distinct from String::HashType so legacy 32-bit hashes cannot enter resource-name APIs through
* an implicit integer conversion. Complete ResourceKey values remain the authoritative identity
* whenever collision safety or persistence is required.
*/
class ResourceNameHash {
public:
constexpr ResourceNameHash() = default;
explicit constexpr ResourceNameHash( Uint64 value ) : mValue( value ) {}
constexpr Uint64 value() const { return mValue; }
explicit constexpr operator bool() const { return mValue != 0; }
constexpr bool operator==( const ResourceNameHash& other ) const {
return mValue == other.mValue;
}
constexpr bool operator!=( const ResourceNameHash& other ) const { return !( *this == other ); }
constexpr bool operator<( const ResourceNameHash& other ) const {
return mValue < other.mValue;
}
private:
Uint64 mValue{ 0 };
};
/**
* Hashes a semantic resource name with the vendored wyhash implementation used by
* UnorderedMap. Do not serialize the result: use the complete ResourceKey in persistent formats.
*/
inline ResourceNameHash resourceNameHash( std::string_view name ) {
return ResourceNameHash( ankerl::unordered_dense::hash<std::string_view>{}( name ) );
}
/** Immutable semantic lookup key. Catalog equality always compares the complete key value. */
class ResourceKey {
public:
@@ -66,6 +103,12 @@ template <> struct hash<EE::Graphics::ResourceId> {
}
};
template <> struct hash<EE::Graphics::ResourceNameHash> {
std::size_t operator()( const EE::Graphics::ResourceNameHash& hash ) const noexcept {
return std::hash<EE::Uint64>{}( hash.value() );
}
};
} // namespace std
#endif

View File

@@ -29,10 +29,12 @@ using ResourceCatalogPtr = ResourcePtr<ResourceCatalog>;
* released after dropping the catalog mutex so resource destruction and callbacks never execute
* while the catalog is locked.
*
* Drawable and font hash lookups use String::hash() of the semantic key as a weak secondary index.
* They are compatibility/convenience lookups and do not replace full-key lookup when collision-safe
* identity is required. Textures use ResourceId for process-wide object identity elsewhere; the
* hash accepted by findDrawable() and findFont() is not a ResourceId.
* Drawable and font hash lookups use resourceNameHash() as a direct secondary index. The dedicated
* 64-bit hash provides a cheap expected-O(1) convenience lookup for trusted names without changing
* the repository-wide 32-bit String::HashType. A hash is not mathematically collision-free, so
* complete ResourceKey lookup remains authoritative and must be used for attacker-controlled names
* or persistent identity. ResourceNameHash values are process-local conveniences and must not be
* serialized.
*
* ResourceCatalog performs no parent, scene, live-registry, filesystem, or fallback search. A
* ResourceScope defines lookup precedence by searching its local catalog and then explicitly
@@ -82,11 +84,17 @@ class EE_API ResourceCatalog {
/** @copydoc findDrawable(const ResourceKey&)const */
DrawablePtr findDrawable( const std::string& key ) const;
/**
* @brief Looks up a drawable through the weak String::hash(key) secondary index.
* @return An owning handle when the indexed drawable is still present, otherwise an empty
* handle.
* @brief Looks up a drawable through the resourceNameHash(key) convenience index.
*/
DrawablePtr findDrawable( const String::HashType& id ) const;
DrawablePtr findDrawable( ResourceNameHash hash ) const;
/**
* @brief Legacy lookup for persisted 32-bit drawable hashes.
*
* This compatibility path scans full bindings and should not be used by new code. Persistent
* formats should migrate to complete ResourceKey values rather than serializing another hash.
* If several names share @p legacyHash, which matching drawable is returned is unspecified.
*/
DrawablePtr findDrawable( String::HashType legacyHash ) const;
/** @return The texture atlas bound to @p key, or an empty handle when it is not present. */
TextureAtlasPtr findAtlas( const ResourceKey& key ) const;
@@ -100,10 +108,9 @@ class EE_API ResourceCatalog {
/** @copydoc findFont(const ResourceKey&)const */
FontPtr findFont( const std::string& key ) const;
/**
* @brief Looks up a font through the weak String::hash(key) secondary index.
* @return An owning handle when the indexed font is still present, otherwise an empty handle.
* @brief Looks up a font through the resourceNameHash(key) convenience index.
*/
FontPtr findFont( const String::HashType& id ) const;
FontPtr findFont( ResourceNameHash hash ) const;
/** @return An owning snapshot of all fonts currently published in this catalog. */
std::vector<FontPtr> getFonts() const;
@@ -155,10 +162,10 @@ class EE_API ResourceCatalog {
mutable System::Mutex mMutex;
UnorderedMap<std::string, TexturePtr> mTextures;
UnorderedMap<std::string, DrawablePtr> mDrawables;
UnorderedMap<String::HashType, DrawableWeakPtr> mDrawablesById;
UnorderedMap<ResourceNameHash, DrawableWeakPtr> mDrawablesByNameHash;
UnorderedMap<std::string, TextureAtlasPtr> mAtlases;
UnorderedMap<std::string, FontPtr> mFonts;
UnorderedMap<String::HashType, FontWeakPtr> mFontsById;
UnorderedMap<ResourceNameHash, FontWeakPtr> mFontsByNameHash;
UnorderedMap<std::string, ShaderProgramPtr> mShaderPrograms;
};

View File

@@ -41,13 +41,14 @@ class EE_API ResourceScope {
DrawablePtr findDrawableSource( const ResourceKey& key ) const;
DrawablePtr findDrawableSource( const std::string& key ) const;
DrawablePtr findDrawable( const std::string& name, bool firstSearchSprite = false ) const;
DrawablePtr findDrawable( const Uint32& id ) const;
DrawablePtr findDrawable( ResourceNameHash hash ) const;
DrawablePtr findDrawable( String::HashType legacyHash ) const;
TextureAtlasPtr findAtlas( const ResourceKey& key ) const;
TextureAtlasPtr findAtlas( const std::string& key ) const;
std::vector<TextureAtlasPtr> getAtlases() const;
FontPtr findFont( const ResourceKey& key ) const;
FontPtr findFont( const std::string& key ) const;
FontPtr findFont( const String::HashType& id ) const;
FontPtr findFont( ResourceNameHash hash ) const;
std::vector<FontPtr> getFonts() const;
ShaderProgramPtr findShaderProgram( const ResourceKey& key ) const;
ShaderProgramPtr findShaderProgram( const std::string& key ) const;

View File

@@ -15,7 +15,8 @@ class EE_API DrawableResolver {
explicit DrawableResolver( Graphics::ResourceScope& resourceScope );
Graphics::DrawablePtr resolve( const std::string& name, bool firstSearchSprite = false ) const;
Graphics::DrawablePtr resolveById( const Uint32& id ) const;
Graphics::DrawablePtr resolveById( Graphics::ResourceNameHash hash ) const;
Graphics::DrawablePtr resolveById( String::HashType legacyHash ) const;
void setPrintWarnings( bool printWarnings );
bool getPrintWarnings() const;

View File

@@ -15,7 +15,7 @@ ResourceScope& FontService::getResourceScope() const {
FontPtr FontService::findHandle( Font* font ) const {
if ( !font )
return {};
FontPtr handle = mResourceScope.findFont( font->getId() );
FontPtr handle = mResourceScope.findFont( font->getName() );
return handle.get() == font ? handle : FontPtr{};
}

View File

@@ -53,19 +53,19 @@ void ResourceCatalog::publishDrawable( std::string key, DrawablePtr drawable ) {
}
DrawablePtr previous;
String::HashType id = String::hash( key );
ResourceNameHash hash = resourceNameHash( key );
{
Lock lock( mMutex );
auto it = mDrawables.find( key );
if ( it == mDrawables.end() ) {
mDrawables.emplace( std::move( key ), drawable );
mDrawablesById[id] = drawable;
mDrawablesByNameHash[hash] = drawable;
return;
}
previous = std::move( it->second );
it->second = drawable;
mDrawablesById[id] = drawable;
mDrawablesByNameHash[hash] = drawable;
}
previous.reset();
@@ -111,18 +111,18 @@ void ResourceCatalog::publishFont( std::string key, FontPtr font ) {
}
FontPtr previous;
String::HashType id = String::hash( key );
ResourceNameHash hash = resourceNameHash( key );
{
Lock lock( mMutex );
auto it = mFonts.find( key );
if ( it == mFonts.end() ) {
mFonts.emplace( std::move( key ), font );
mFontsById[id] = font;
mFontsByNameHash[hash] = font;
return;
}
previous = std::move( it->second );
it->second = font;
mFontsById[id] = font;
mFontsByNameHash[hash] = font;
}
previous.reset();
}
@@ -172,10 +172,19 @@ DrawablePtr ResourceCatalog::findDrawable( const std::string& key ) const {
return it != mDrawables.end() ? it->second : DrawablePtr{};
}
DrawablePtr ResourceCatalog::findDrawable( const String::HashType& id ) const {
DrawablePtr ResourceCatalog::findDrawable( ResourceNameHash hash ) const {
Lock lock( mMutex );
auto it = mDrawablesById.find( id );
return it != mDrawablesById.end() ? it->second.lock() : DrawablePtr{};
auto it = mDrawablesByNameHash.find( hash );
return it != mDrawablesByNameHash.end() ? it->second.lock() : DrawablePtr{};
}
DrawablePtr ResourceCatalog::findDrawable( String::HashType legacyHash ) const {
Lock lock( mMutex );
for ( const auto& [key, drawable] : mDrawables ) {
if ( String::hash( key ) == legacyHash )
return drawable;
}
return {};
}
TextureAtlasPtr ResourceCatalog::findAtlas( const ResourceKey& key ) const {
@@ -207,10 +216,10 @@ FontPtr ResourceCatalog::findFont( const std::string& key ) const {
return it != mFonts.end() ? it->second : FontPtr{};
}
FontPtr ResourceCatalog::findFont( const String::HashType& id ) const {
FontPtr ResourceCatalog::findFont( ResourceNameHash hash ) const {
Lock lock( mMutex );
auto it = mFontsById.find( id );
return it != mFontsById.end() ? it->second.lock() : FontPtr{};
auto it = mFontsByNameHash.find( hash );
return it != mFontsByNameHash.end() ? it->second.lock() : FontPtr{};
}
std::vector<FontPtr> ResourceCatalog::getFonts() const {
@@ -275,9 +284,7 @@ bool ResourceCatalog::eraseDrawable( const std::string& key ) {
drawable = std::move( it->second );
mDrawables.erase( it );
auto idIt = mDrawablesById.find( String::hash( key ) );
if ( idIt != mDrawablesById.end() )
mDrawablesById.erase( idIt );
mDrawablesByNameHash.erase( resourceNameHash( key ) );
}
drawable.reset();
@@ -315,9 +322,7 @@ bool ResourceCatalog::eraseFont( const std::string& key ) {
return false;
font = std::move( it->second );
mFonts.erase( it );
auto idIt = mFontsById.find( String::hash( key ) );
if ( idIt != mFontsById.end() )
mFontsById.erase( idIt );
mFontsByNameHash.erase( resourceNameHash( key ) );
}
font.reset();
return true;
@@ -334,9 +339,7 @@ bool ResourceCatalog::eraseFont( Font* font ) {
return false;
removed = std::move( it->second );
mFonts.erase( it );
auto idIt = mFontsById.find( font->getId() );
if ( idIt != mFontsById.end() )
mFontsById.erase( idIt );
mFontsByNameHash.erase( resourceNameHash( font->getName() ) );
}
removed.reset();
return true;
@@ -363,28 +366,28 @@ bool ResourceCatalog::eraseShaderProgram( const std::string& key ) {
void ResourceCatalog::clear() {
UnorderedMap<std::string, TexturePtr> textures;
UnorderedMap<std::string, DrawablePtr> drawables;
UnorderedMap<String::HashType, DrawableWeakPtr> drawablesById;
UnorderedMap<ResourceNameHash, DrawableWeakPtr> drawablesByNameHash;
UnorderedMap<std::string, TextureAtlasPtr> atlases;
UnorderedMap<std::string, FontPtr> fonts;
UnorderedMap<String::HashType, FontWeakPtr> fontsById;
UnorderedMap<ResourceNameHash, FontWeakPtr> fontsByNameHash;
UnorderedMap<std::string, ShaderProgramPtr> shaderPrograms;
{
Lock lock( mMutex );
textures = std::move( mTextures );
drawables = std::move( mDrawables );
drawablesById = std::move( mDrawablesById );
drawablesByNameHash = std::move( mDrawablesByNameHash );
atlases = std::move( mAtlases );
fonts = std::move( mFonts );
fontsById = std::move( mFontsById );
fontsByNameHash = std::move( mFontsByNameHash );
shaderPrograms = std::move( mShaderPrograms );
}
textures.clear();
drawables.clear();
drawablesById.clear();
drawablesByNameHash.clear();
atlases.clear();
fonts.clear();
fontsById.clear();
fontsByNameHash.clear();
shaderPrograms.clear();
}

View File

@@ -121,12 +121,12 @@ FontPtr ResourceScope::findFont( const std::string& key ) const {
return {};
}
FontPtr ResourceScope::findFont( const String::HashType& id ) const {
if ( FontPtr font = mLocalCatalog->findFont( id ) )
FontPtr ResourceScope::findFont( ResourceNameHash hash ) const {
if ( FontPtr font = mLocalCatalog->findFont( hash ) )
return font;
Lock lock( mMutex );
for ( const ResourceCatalogPtr& catalog : mImports ) {
if ( FontPtr font = catalog->findFont( id ) )
if ( FontPtr font = catalog->findFont( hash ) )
return font;
}
return {};
@@ -288,12 +288,24 @@ DrawablePtr ResourceScope::findDrawable( const std::string& name, bool firstSear
return texture ? texture->clone() : DrawablePtr{};
}
DrawablePtr ResourceScope::findDrawable( const Uint32& id ) const {
DrawablePtr source = mLocalCatalog->findDrawable( id );
DrawablePtr ResourceScope::findDrawable( ResourceNameHash hash ) const {
DrawablePtr source = mLocalCatalog->findDrawable( hash );
if ( !source ) {
Lock lock( mMutex );
for ( const ResourceCatalogPtr& catalog : mImports ) {
if ( ( source = catalog->findDrawable( id ) ) )
if ( ( source = catalog->findDrawable( hash ) ) )
break;
}
}
return source ? source->clone() : DrawablePtr{};
}
DrawablePtr ResourceScope::findDrawable( String::HashType legacyHash ) const {
DrawablePtr source = mLocalCatalog->findDrawable( legacyHash );
if ( !source ) {
Lock lock( mMutex );
for ( const ResourceCatalogPtr& catalog : mImports ) {
if ( ( source = catalog->findDrawable( legacyHash ) ) )
break;
}
}
@@ -383,7 +395,7 @@ bool ResourceScope::eraseLocalFont( const std::string& key ) {
bool ResourceScope::eraseLocalFont( Font* font ) {
if ( !font )
return false;
FontPtr handle = mLocalCatalog->findFont( font->getId() );
FontPtr handle = mLocalCatalog->findFont( font->getName() );
if ( handle.get() != font )
return false;
if ( !mLocalCatalog->eraseFont( font ) )

View File

@@ -155,11 +155,20 @@ DrawablePtr DrawableResolver::resolve( const std::string& name, bool firstSearch
return drawable;
}
DrawablePtr DrawableResolver::resolveById( const Uint32& id ) const {
DrawablePtr DrawableResolver::resolveById( ResourceNameHash hash ) const {
ResourceScope& resourceScope = mSceneNode ? *mSceneNode->getResourceScope() : *mResourceScope;
DrawablePtr drawable = resourceScope.findDrawable( id );
DrawablePtr drawable = resourceScope.findDrawable( hash );
if ( !drawable && mPrintWarnings )
Log::warning( "DrawableResolver::resolveById: \"%ld\" not found", id );
Log::warning( "DrawableResolver::resolveById: \"%llu\" not found",
static_cast<unsigned long long>( hash.value() ) );
return drawable;
}
DrawablePtr DrawableResolver::resolveById( String::HashType legacyHash ) const {
ResourceScope& resourceScope = mSceneNode ? *mSceneNode->getResourceScope() : *mResourceScope;
DrawablePtr drawable = resourceScope.findDrawable( legacyHash );
if ( !drawable && mPrintWarnings )
Log::warning( "DrawableResolver::resolveById: legacy hash \"%u\" not found", legacyHash );
return drawable;
}

View File

@@ -466,6 +466,51 @@ UTEST( ResourcePrerequisites, resourceCatalogRemovalPreservesRetainedDrawableCon
Engine::destroySingleton();
}
UTEST( ResourcePrerequisites, resourceNameHashSeparatesLegacyDrawableCollision ) {
constexpr const char* firstKey = "BA";
constexpr const char* secondKey = "Ab";
ASSERT_EQ( String::hash( firstKey ), String::hash( secondKey ) );
const ResourceNameHash firstHash = resourceNameHash( firstKey );
const ResourceNameHash secondHash = resourceNameHash( secondKey );
ASSERT_NE( firstHash.value(), secondHash.value() );
auto catalog = ResourceCatalog::New();
auto first = makeResource<TestDrawableResource>();
auto second = makeResource<TestDrawableResource>();
catalog->publishDrawable( firstKey, first );
catalog->publishDrawable( secondKey, second );
EXPECT_EQ( first.get(), catalog->findDrawable( firstHash ).get() );
EXPECT_EQ( second.get(), catalog->findDrawable( secondHash ).get() );
EXPECT_TRUE( catalog->eraseDrawable( firstKey ) );
EXPECT_TRUE( catalog->findDrawable( firstHash ) == nullptr );
EXPECT_EQ( second.get(), catalog->findDrawable( secondHash ).get() );
EXPECT_TRUE( catalog->eraseDrawable( secondKey ) );
EXPECT_TRUE( catalog->findDrawable( secondHash ) == nullptr );
}
UTEST( ResourcePrerequisites, resourceNameHashSeparatesLegacyFontCollision ) {
constexpr const char* firstKey = "BA";
constexpr const char* secondKey = "Ab";
ASSERT_EQ( String::hash( firstKey ), String::hash( secondKey ) );
const ResourceNameHash firstHash = resourceNameHash( firstKey );
const ResourceNameHash secondHash = resourceNameHash( secondKey );
ASSERT_NE( firstHash.value(), secondHash.value() );
auto scope = ResourceScope::New();
auto first = FontBMFont::New( firstKey, *scope );
auto second = FontBMFont::New( secondKey, *scope );
auto catalog = scope->getLocalCatalog();
EXPECT_EQ( first.get(), catalog->findFont( firstHash ).get() );
EXPECT_EQ( second.get(), catalog->findFont( secondHash ).get() );
EXPECT_TRUE( scope->eraseLocalFont( firstKey ) );
EXPECT_TRUE( catalog->findFont( firstHash ) == nullptr );
EXPECT_EQ( second.get(), catalog->findFont( secondHash ).get() );
EXPECT_TRUE( scope->eraseLocalFont( secondKey ) );
EXPECT_TRUE( catalog->findFont( secondHash ) == nullptr );
}
UTEST( ResourcePrerequisites, resourceScopesResolveOnlyLocalAndExplicitlyImportedCatalogs ) {
EE::Window::Window* window = createLifecycleTestWindow( "Resource scope isolation test" );
TextureFactory* factory = TextureFactory::instance();