HashFnv1a32 / HashFnv1a64
HashFnv1a32 turns a string literal into a constant at compile time. That is the one thing no other hash in Basalt can do, and it is what makes a switch over names possible:
switch (HashFnv1a32(stage_name)){case HashFnv1a32("vertex"): return ShaderStage::Vertex;case HashFnv1a32("fragment"): return ShaderStage::Fragment;case HashFnv1a32("compute"): return ShaderStage::Compute;default: return ShaderStage::Unknown;}Every case label above is an integer in the binary — the compiler runs the hash, not your program.
FNV-1a is the weakest hash Basalt ships, and that is a deliberate trade, not an oversight. It exists for two jobs: a name known at compile time, and reproducing a value another system computed. It must never key a hash map — HashFast is the function for that, and the reason is below.
Compile-time identifiers
Section titled “Compile-time identifiers”A string identifier costs nothing at run time if the compiler resolves it. HashFnv1a32 and HashFnv1a64 are constexpr, so anywhere a constant is legal, the hash of a literal is legal too:
constexpr u32 sky_material = HashFnv1a32("materials/sky");
static_assert(HashFnv1a64("a") == 0xAF63DC4C8601EC8Cull); // RFC 9923's own vector
// A table of assets keyed by name, resolved before main() runs.struct AssetEntry { u32 name_hash; AssetId id; };constexpr AssetEntry assets[] = { { HashFnv1a32("materials/sky"), AssetId::Sky }, { HashFnv1a32("materials/water"), AssetId::Water },};The string-literal form is compile-time only, on purpose: HashFnv1a32("...") is an immediate call the compiler must evaluate, so a char buffer filled at run time will not compile through it. That closes a real trap — a half-filled char name[64] would otherwise silently hash 63 bytes, padding included. To hash bytes you have at run time, use the String8 form, which is the same function and the same values:
const u32 id = HashFnv1a32(Str8FromCString(name_from_the_file)); // run time, same valueNote that HashFnv1a32(Str8FromCString("literal")) is a run-time call: Str8FromCString() scans for the terminator and cannot run in a constant expression. Pass the literal directly when you want the constant.
Interop: reproducing another system’s value
Section titled “Interop: reproducing another system’s value”HashFnv1a32 and HashFnv1a64 are RFC 9923’s FNV-1a, frozen for the life of the framework, and pinned in Basalt’s build by the RFC’s own published test vectors. Anything that emits an FNV-1a value emits one of these:
| System | What it hashes with FNV-1a |
|---|---|
| Roslyn (C#/VB) | the synthesized hash behind every switch over strings — 32-bit |
| Unreal Engine | FFnv::MemFnv32 / MemFnv64, UE::HashStringFNV1a |
| EnTT | entt::hashed_string, i.e. every "..."_hs asset and type id — 32-bit by default |
| Kubernetes | the pod-template-hash label that names a ReplicaSet — 32-bit |
| Go | hash/fnv — New32a, New64a |
| MSVC’s STL | std::hash for scalars and strings, internally |
Two things decide whether your value matches theirs, and both are yours to get right:
- What bytes go in. Basalt hashes bytes. Roslyn hashes UTF-16 code units, so reproducing a Roslyn value means feeding the same UTF-16 units, not the UTF-8 text.
- Whether a terminating zero is included.
String8carries a size and never a NUL, so Basalt gives you RFC 9923’s string value. A C system that hashedstrlen(s) + 1bytes produced its block value — pass the zero byte yourself to match it:
HashFnv1a32(Str8FromCString("foobar")) // 0xBF9CF968 — the stringHashFnv1a32(Str8(bytes_with_nul, 7)) // 0x0C1C9EB8 — the string plus its NULHashing a stream, or several pieces
Section titled “Hashing a stream, or several pieces”For FNV the running value is the whole state, so continuing a hash is passing the previous value back in — the shape zlib’s crc32 uses:
u32 hash = FNV1A_32_OFFSET_BASIS;while (ReadChunk(file, chunk)){ hash = HashFnv1a32Extend(hash, chunk);}HashFnv1a32Extend(HashFnv1a32Extend(basis, a), b) is exactly HashFnv1a32(a ++ b), at every split point. Two consequences worth knowing:
- The hash is not prefix-free: hashing
"ab"then"c"gives the same value as"a"then"bc". If you fold several fields into one hash, fold in your own separator or length. - Start from the offset basis, never from a value you invented. The state
0is absorbing: zero bytes folded into a state of 0 leave it at 0, forever. The offset basis never reaches 0, so nothing goes wrong unless you pick your own start.
What it does not promise
Section titled “What it does not promise”Never key a table on it
Section titled “Never key a table on it”FNV-1a does not avalanche, and not by a small margin. One multiply per byte cannot carry the last byte’s contribution to the top of the word, so the value’s entropy sits at the bottom and the top barely moves. Measured over all 256 one-byte inputs, the top byte of the 64-bit hash takes exactly one value while the bottom byte takes all 256; over 4096 three-byte keys the top byte still takes only 21 of 256.
An open-addressed hash map takes its slot index from one end of the hash and a fingerprint from the other, so a table built on this value hands every short key the same fingerprint and its probe chains collapse. Use HashFast for anything you look up. If you genuinely need a narrower value, fold rather than truncate — ((hash >> 8) ^ hash) & mask, the reduction FNV’s own author prescribes.
Which hash, in one line each
Section titled “Which hash, in one line each”| You need | Use |
|---|---|
| A constant from a name, at compile time | HashFnv1a32 / HashFnv1a64 |
| To reproduce an FNV value another system computed | HashFnv1a32 / HashFnv1a64 |
A key for a HashMap or any in-memory lookup |
HashFast |
| A 64-bit identity for content you persist | HashRapidV3Nano |
API summary
Section titled “API summary”Parameters, assertions and the full contract are documented in basalt/core/hash/HashFnv1a.h.
| Signature | Purpose |
|---|---|
HashFnv1a32(String8 bytes) |
The frozen 32-bit FNV-1a hash of the bytes. constexpr. |
HashFnv1a64(String8 bytes) |
The same at 64 bits. |
HashFnv1a32("literal") |
The hash of a string literal, evaluated at compile time, terminator excluded. |
HashFnv1a64("literal") |
The same at 64 bits. |
HashFnv1a32Extend(u32 previous, String8 bytes) |
Continues a hash over more bytes. Start at FNV1A_32_OFFSET_BASIS. |
HashFnv1a64Extend(u64 previous, String8 bytes) |
The same at 64 bits, starting at FNV1A_64_OFFSET_BASIS. |
FNV1A_32_OFFSET_BASIS, FNV1A_32_PRIME |
RFC 9923’s 32-bit constants. |
FNV1A_64_OFFSET_BASIS, FNV1A_64_PRIME |
RFC 9923’s 64-bit constants. |