How Shader Hot Reloading Works in Equilibrium Engine Using x-watcher

The Equilibrium Engine implements shader hot reloading by combining ECS components with the x-watcher file monitoring library to detect shader changes, flag entities for updates, and recompile BGFX programs without restarting the engine.

Shader iteration is a critical workflow in graphics development. The Equilibrium Engine, an open-source ECS-based game engine, solves this by integrating the x-watcher library into its component system. This architecture allows vertex and fragment shaders to be recompiled and swapped at runtime while the engine continues rendering, eliminating the need to restart the application for every shader tweak.

The Three-Subsystem Architecture

The implementation relies on three tightly coupled subsystems working across the main and background threads:

Subsystem Role
ECS Components (HotReloadableShader, GfxResource, ReloadShader, FileWatcher) Store shader file paths, BGFX program handles, and flags that signal when a reload is required.
x-watcher Runs a background thread monitoring vertex and fragment shader files. When a file is modified, it invokes a C callback on the main thread.
Hot-reload system (HotReloadShaders observer) Reacts to the ReloadShader flag, recompiles the shader, destroys the old BGFX program, and updates the component that uses it.

Registering Shader Files for Monitoring

When a shader program is created, the engine must register its source files with the watcher. This happens automatically through the create_program macro defined in equilibrium/utils/bgfx_utils.h.

The create_program Macro

The macro creates a HotReloadableShader component that records the absolute paths of the vertex and fragment shader files:

ecs_set(program_entity.world, program_entity.handle,
        HotReloadableShader,
        {(entity_t){entity.handle, entity.world},
         ecs_id(T), ECS_SIZEOF(T), offsetof(T, member_name),
         vertex_handle.file_path, fragment_handle.file_path});

Source: equilibrium/utils/bgfx_utils.h – lines 58‑61

InitHotReloadShaders Observer

When the HotReloadableShader component is attached, the InitHotReloadShaders observer (triggered by EcsOnSet) runs. It creates two xWatcher_reference objects and registers them with the global FileWatcher component:

xWatcher_reference vertex, fragment;
vertex.path = shader.vertex_shader_name;
fragment.path = shader.fragment_shader_name;
vertex.callback_func = fragment.callback_func = callback_func;
fragment.context = vertex.context = it->entities[i];
fragment.additional_data = vertex.additional_data = it->world;

xWatcher_appendFile(watcher->data, &vertex);
xWatcher_appendFile(watcher->data, &fragment);

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 94‑106

The FileWatcher component is created once when the system imports:

FileWatcher watcher = {xWatcher_create()};
ecs_set_ptr(world, ecs_id(FileWatcher), FileWatcher, &watcher);

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 89‑91

Detecting File Changes with x-watcher

The x-watcher library runs a dedicated background thread (using pthread on Linux or OVERLAPPED I/O on Windows) that monitors the file system. When a watched shader file is modified, it invokes the registered callback on the main thread.

The Callback Function

The callback_func receives the file event and entity context, then sets the ReloadShader component to flag the entity for reloading:

void callback_func(XWATCHER_FILE_EVENT event,
                   const char *path,
                   int context,
                   void *data) {
    if (event == XWATCHER_FILE_MODIFIED) {
        entity_t shader_entity = (entity_t){context, data};
        ecs_set(shader_entity.world,
                shader_entity.handle,
                ReloadShader, {});
    }
}

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 22‑42

This design keeps the hot-reload work on the main thread where the ECS can safely update resources, while the heavy file-system monitoring happens in the background.

Performing the Hot Reload

The HotReloadShaders observer is triggered whenever an entity has the three components HotReloadableShader, GfxResource, and ReloadShader (using EcsOnSet):

ECS_OBSERVER(world, HotReloadShaders,
             EcsOnSet,
             HotReloadableShader,
             GfxResource,
             ReloadShader);

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 96‑98

Inside the observer, the engine performs four critical steps:

  1. Reload shader binaries using shader_load to read the updated files from disk and create new BGFX shader handles.
  2. Destroy the old BGFX program using bgfx_destroy_program.
  3. Create a new program by linking the new vertex and fragment shaders with bgfx_create_program.
  4. Update the component data by writing the new program handle back into the owning component using pointer arithmetic based on the stored field offset.
ShaderHandle vertex_handle   = shader_load(entity.world,
                                          shader.vertex_shader_name, true);
ShaderHandle fragment_handle = shader_load(entity.world,
                                          shader.fragment_shader_name, true);

bgfx_destroy_program((bgfx_program_handle_t){resource.handle});
resource.handle = bgfx_create_program(vertex_handle.handle,
                                      fragment_handle.handle,
                                      true).idx;

void *component = ecs_get_mut_id(shader.shader_entity.world,
                                 shader.shader_entity.handle,
                                 shader.component_id);
*(bgfx_program_handle_t *)(component + shader.filed_offset) =
    (bgfx_program_handle_t){resource.handle};

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 139‑159

Finally, the observer logs the successful reload:

ecs_trace("Hot reload successful for %s | %s shader program: %s",
          shader.vertex_shader_name,
          shader.fragment_shader_name,
          entity_get_name(entity));

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 167‑168

Frame-by-Frame Watcher Updates

To ensure file system events are processed, the UpdateFileWatcher system runs every frame (tagged with the custom OnInput phase). It simply forwards the update call to the underlying xWatcherUpdate function:

static void UpdateFileWatcher(ecs_iter_t *it) {
    FileWatcher *watcher = ecs_field(it, FileWatcher, 1);
    xWatcherUpdate(watcher->data);
}

Source: equilibrium/systems/rendering/gfx_resource_system.c – lines 74‑76

This polling approach ensures that the background thread's events are dispatched to the main thread callbacks without blocking the render loop.

Summary

  • ECS-driven architecture: Shader hot reloading in Equilibrium Engine uses components (HotReloadableShader, ReloadShader, FileWatcher) to track state and trigger updates.
  • x-watcher integration: The third-party x-watcher library provides cross-platform file monitoring via background threads, invoking callbacks when shader files change.
  • Deferred reload pattern: File change callbacks only set a flag (ReloadShader), keeping heavy recompilation work on the main thread where ECS operations are safe.
  • Automatic handle management: The HotReloadShaders observer destroys old BGFX programs, creates new ones from reloaded binaries, and updates component memory using stored field offsets.
  • Continuous polling: The UpdateFileWatcher system processes file events every frame, ensuring the background thread remains responsive without blocking rendering.

Frequently Asked Questions

What is x-watcher and why does Equilibrium Engine use it?

x-watcher is a lightweight, cross-platform C library for file system monitoring that abstracts platform-specific APIs like Linux inotify and Windows OVERLAPPED I/O. Equilibrium Engine uses it because it provides non-blocking file watching via background threads, allowing the engine to detect shader modifications without freezing the main render loop or implementing platform-specific code.

How does the engine avoid blocking the render loop during file monitoring?

File monitoring runs on a background thread managed by x-watcher, while the main thread continues rendering. When a file changes, x-watcher invokes a callback that merely sets an empty ReloadShader component on the affected entity. The actual heavy work—reading files, compiling shaders, and creating new BGFX programs—happens later on the main thread when the HotReloadShaders observer runs, ensuring thread safety with ECS operations.

What happens to existing shader programs when a file is modified?

The old BGFX program is destroyed and replaced atomically from the perspective of the rendering system. When the HotReloadShaders observer triggers, it calls bgfx_destroy_program on the existing handle, loads the new shader binaries using shader_load, creates a new program with bgfx_create_program, and writes the new handle back into the component's memory. The next frame automatically uses the updated program without requiring entity recreation.

Can hot reloading be disabled for specific shaders?

Yes, by omitting the HotReloadableShader component during program creation. The create_program macro in bgfx_utils.h automatically registers shaders for hot reloading, but developers can manually create BGFX programs using bgfx_create_program without setting the HotReloadableShader component. Without this component, no file watcher references are created, and the shader will not participate in the hot-reload pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →