2.2 · Window / Platform
The Window abstraction is the platform seam. It hides whether we're running on a real desktop with GLFW and Vulkan, or headless with no surface at all, and it owns the NVRHI DeviceManager that the renderer talks to.
GLFWWindow owns the real OS handle and the NVRHI DeviceManager; NullWindow is a do-nothing stub for headless builds.Overview
Window in Hazel/src/Hazel/Core/Window.h is a small abstract interface — size, title, vsync, swap-chain-equivalent calls,
event callback registration. The engine talks to this interface everywhere; only platform-specific code touches the concrete subclasses.
Today there are exactly two concretions. The split lets us run the same engine binary in three very different shells: the editor with a real GUI, the runtime as a normal app, and CI / RL training as a headless process that never opens a window.
GLFWWindow
Desktop window. Wraps a GLFWwindow*, hooks into GLFW's event callbacks, and owns the NVRHI DeviceManager that brings up the Vulkan device, queues, and swap chain.
NullWindow
Headless stub. No OS window, no swap chain, no device-presented frames. Used by HeadlessApplication, RL training, server builds, and tests where rendering output is captured off-screen via NVRHI directly.
Ownership & lifecycle
Application constructs the window during init — the policy choice between GLFW and Null is made by the client subclass, not by user code. From then on:
- Application owns the
WindowbyRef<>; tearing down the app destroys it. - GLFWWindow owns the
GLFWwindow*native handle and the NVRHIDeviceManager*. Both are released in the destructor — do not callglfwDestroyWindowfrom elsewhere. - NullWindow owns nothing platform-level. Its virtuals exist only to satisfy the interface.
Anything that holds onto the window for the lifetime of a frame should take a Ref<Window> or a Ref<GLFWWindow>;
anything that observes momentarily should take const Ref<Window>&. See Cross-Cutting for smart-pointer rules.
The GetNativeWindow() pitfall
GetNativeWindow() is defined on GLFWWindow, not on the abstract base. There is no equivalent on NullWindow because there is no native window.
Code that needs the raw handle must hold a Ref<GLFWWindow>, not a Ref<Window>.
The convenient escape hatch: Application::GetWindow() already returns Ref<GLFWWindow> (the cast is done for you), so most callers
get the native handle for free without any visible cast. Code that explicitly stores its own Ref<Window> handle — rare — must call
.As<GLFWWindow>() before reaching for GetNativeWindow(), and must be guarded with a feature check if it can run in a headless build.
Adding new virtuals
Any new pure-virtual added to Window must be implemented on both GLFWWindow and NullWindow.
Headless builds link NullWindow — if it doesn't compile, CI breaks first.
When the headless implementation has nothing meaningful to do, the stub should be a no-op that returns a default-constructed value (and ideally logs a one-line warning if called in a context where it really shouldn't have been reached). Avoid throwing — the headless path runs unattended and must keep going.
DeviceManager ownership
GLFWWindow brings up the NVRHI DeviceManager during construction: it creates the Vulkan instance, picks a physical device, creates the
logical device and queues, then builds the swap chain bound to the GLFW surface. The rest of the renderer (see
2.3 Renderer) only knows about the DeviceManager* — it never reaches for GLFW.
In headless mode the renderer still asks for a device; the runtime / headless code path constructs an NVRHI device directly without involving a window
surface. That is owned outside NullWindow, not by it.
Key types
| Type | Header | Role |
|---|---|---|
Window | Hazel/src/Hazel/Core/Window.h | Abstract platform seam. Size, title, vsync, event callback, frame begin/end. |
GLFWWindow | Hazel/src/Hazel/Platform/GLFW/GLFWWindow.h | Desktop concretion. Owns GLFWwindow* and the NVRHI DeviceManager*. |
NullWindow | Hazel/src/Hazel/Platform/Null/NullWindow.h | Headless stub. Implements every Window virtual as a no-op. |
WindowSpecification | Window.h | POD config — title, width, height, decoration flags. |
DeviceManager | NVRHI (vendor) | Vulkan device + swap chain wrapper. Lives behind the window in the desktop path. |
Extending
Most platform-shaped work falls into one of these:
| You want… | Do this |
|---|---|
| A new query on every window (DPI, monitor, …) | Add a pure virtual to Window, implement on both concretions. |
| A GLFW-only feature (cursor capture, raw mouse motion) | Add a non-virtual method on GLFWWindow, gate callers behind a Ref<GLFWWindow> cast. |
| A new platform (Wayland-direct, SDL, mobile) | New folder under Hazel/src/Hazel/Platform/<Platform>/ with a new Window subclass. Wire selection in premake5.lua. |
| Headless rendering target | The path already exists — use NullWindow + manually-constructed NVRHI device. See the runtime/headless app for a worked example. |
Pitfalls
Only Hazel/src/Hazel/Platform/GLFW/ should include GLFW headers. Engine systems that need window-shaped data must go through the Window interface so headless builds keep working.
ApplicationThere's exactly one window per process and it's owned by Application. If you find yourself reaching for new GLFWWindow, you're on the wrong path — lift whatever you're building into the application init flow.