String Heap

Overview

Strings cannot be stored directly in the Words buffer because they are variable-length.
Instead, Fusion provides NetworkedStringHeap, a per-object heap that stores string data and exposes fixed-size handles for use in the Words buffer.

NetworkedStringHeap

Each Object (both ObjectRoot and ObjectChild) contains a NetworkedStringHeap initialized with 1024 bytes of storage:

C++

class Object {
    NetworkedStringHeap StringHeap{1024};
    // ...
};

Sub-objects (ObjectChild) delegate string operations to their root's heap via Root()->StringHeap when the sub-object's own heap is not sufficient.
The root heap is the authoritative storage for the object hierarchy.

StringHandle

A StringHandle is a fixed-size reference (2 words / 8 bytes) stored in the Words buffer:

C++

struct StringHandle {
    uint32_t id;          // Slot index in the heap
    uint32_t generation;  // Generation counter for use-after-free detection
};
Field Description
id Index into the heap's entry table. 0 is the invalid/empty handle.
generation Incremented each time a slot is reused. Stale handles (wrong generation) are rejected.

In the Words buffer, a StringHandle occupies exactly 2 words at the property's offset:

  • words[offset] = handle.id
  • words[offset + 1] = handle.generation

Lifecycle

Allocate

C++

StringHandle Object::AddString(const PhotonCommon::CharType* str);

Allocates heap storage, copies the UTF-8 string data and returns a handle.
The handle's id and generation are then written into the Words buffer by the integration layer.

For empty strings, do not call AddString.
Instead, write id = 0 directly.
The SDK treats id == 0 as an invalid handle that resolves to an empty string.

Resolve

C++

const PhotonCommon::CharType* Object::ResolveString(
    const StringHandle& handle,
    StringMessage& OutStatus
);

Looks up the string data for the given handle.
Returns a pointer to the stored UTF-8 data and sets OutStatus to indicate success or failure.

Free

C++

StringHandle Object::FreeString(const StringHandle& handle);

Releases the heap storage associated with the handle.
Returns an invalidated handle (id = 0).
The freed slot's generation is incremented, so any stale handles pointing to it will fail validation.

You must call FreeString before allocating a replacement string for the same property.
Failing to do so leaks heap space.

Validation

C++

bool Object::IsValidStringHandle(const StringHandle& handle);

Returns true if the handle points to a live entry with a matching generation.

Helpers

C++

uint32_t Object::GetStringLength(const StringHandle& handle);
void Object::LogStringData(const StringHandle& handle);

StringMessage Status Codes

ResolveString reports a status code indicating the result:

C++

enum class StringMessage {
    Valid          = 0,   // String resolved successfully
    NotALiveEntry  = 1,   // Slot has been freed
    WrongGeneration = 2,  // Handle is stale (slot was reused)
    OutOfRange     = 3,   // Handle id exceeds entry table bounds
    WrongSize      = 4,   // Internal size mismatch
    EmptyString    = 5,   // Slot exists but contains empty data
    InvalidHandle  = 6,   // Handle id is 0 (null handle)
};
Status Meaning Action
Valid String resolved successfully Use the returned pointer
NotALiveEntry Slot has been freed Treat as no string available
WrongGeneration Handle is stale (slot was reused) Treat as no string available
OutOfRange Handle id exceeds entry table bounds Treat as no string available
WrongSize Internal size mismatch Treat as no string available
EmptyString Slot exists but contains empty data Treat as no string available
InvalidHandle Handle id is 0 (null handle) Treat as no string available

Only Valid returns usable string data.
All other statuses should be treated as "no string available."

Heap Internals

Entry Table

The heap maintains a table of Entry structs:

C++

struct Entry {
    uint32_t offset;       // Byte offset into StringData buffer
    uint32_t size;         // String length in bytes
    uint32_t generation;   // Current generation for this slot
    bool     alive;        // Whether this slot is in use

    bool     IsDirty;      // Needs replication
    Tick     ChangedTick;  // When the entry was last modified
};

Memory Layout

Region Contents Notes
Slot 0 str_0 Live entry
Slot 1 str_1 Live entry
Slot 2 (freed) Tracked by free_by_offset
Slot 3 str_3 Live entry
Remaining (free) Available for allocation

Free List

Freed slots are tracked in free_ids (sorted set with lowest index preferred for reuse).
Freed heap segments are tracked in free_by_offset and coalesced to reduce fragmentation:

C++

struct FreeSeg {
    uint32_t offset;
    uint32_t size;
};

Compaction

compact_heap() defragments the string data buffer by moving live entries to fill gaps left by freed strings.
Called internally when fragmentation reaches a threshold.

Auto-Resize

The heap starts at 1024 bytes and grows automatically when allocation cannot find a contiguous free segment.
The constant HEAP_BUFFER_PADDING (256 bytes) provides a growth margin.

Replication

The heap has its own replication path separate from the Words buffer.
The SDK tracks dirty entries via IsDirty and ChangedTick, and includes string heap data in state packets only when entries or data have changed:

C++

constexpr uint8_t OBJECT_SENDFLAG_STRINGHEAP_ENTRIES_CHANGE = 2;
constexpr uint8_t OBJECT_SENDFLAG_STRINGHEAP_DATA_CHANGE    = 4;

The heap also maintains its own Shadow and Ticks buffers for change detection, analogous to the Words buffer's shadow.

Spawn Data Exception

During object creation, strings in spawn data (the header blob) must be serialized as raw bytes rather than as StringHandle references.
The heap is available immediately after object allocation, but any strings written to Words during the initial population (before SetHasValidData(true)) follow the normal handle flow.

The Header field is opaque bytes that the SDK passes through without interpreting.
If your spawn data includes strings, encode them as length-prefixed UTF-8 in the header, not as StringHandles.

Sub-Object Delegation

Sub-objects (ObjectChild) have their own StringHeap instance, but string operations called on an ObjectChild may delegate to the root's heap depending on the operation.
The key behavior:

  • AddString / FreeString / ResolveString operate on the object's own heap.
  • The root heap is the primary heap for replication purposes.
  • All string handles within a hierarchy must track which object's heap they belong to.

Handle Tracking for Leak Prevention

Every AddString call must eventually be paired with a FreeString call, either when the property value changes or when the object is destroyed.
Leaked handles waste heap space and can eventually exhaust the heap.

A recommended pattern:

C++

// Track handles per property
struct StringProperty {
    StringHandle handle{0, 0};
    int32_t wordOffset;

    void Set(Object* obj, const PhotonCommon::CharType* str) {
        // Free old handle
        if (handle.id != 0) {
            obj->FreeString(handle);
        }

        // Allocate new
        if (str && str[0] != 0) {
            handle = obj->AddString(str);
        } else {
            handle = {0, 0};
        }

        // Write to Words
        obj->Words.Ptr[wordOffset]     = handle.id;
        obj->Words.Ptr[wordOffset + 1] = handle.generation;
    }

    void Free(Object* obj) {
        if (handle.id != 0) {
            obj->FreeString(handle);
            handle = {0, 0};
        }
    }
};

Usage Patterns

Authority Side (Writing)

C++

// Read the current handle from Words
StringHandle oldHandle;
oldHandle.id         = words[offset];
oldHandle.generation = words[offset + 1];

// Free the old string (if valid)
if (oldHandle.id != 0) {
    obj->FreeString(oldHandle);
}

// Allocate new string
StringHandle newHandle = obj->AddString(PHOTON_STR("Hello, world!"));

// Write new handle to Words
words[offset]     = newHandle.id;
words[offset + 1] = newHandle.generation;

Remote Side (Reading)

C++

// Read handle from Words
StringHandle handle;
handle.id         = words[offset];
handle.generation = words[offset + 1];

if (handle.id == 0) {
    // Empty string
} else {
    StringMessage status;
    const PhotonCommon::CharType* str = obj->ResolveString(handle, status);
    if (status == StringMessage::Valid && str) {
        // Use str (UTF-8 encoded)
    }
}
  • {VersionPath}/manual/architecture -- Words buffer layout and type mapping
  • {VersionPath}/manual/scene-management -- Spawn data exception during object creation
Back to top