October 24, 2025

Sidreal Engine 2025

C++OpenGLRenderingECS 5 min read
Sidreal Engine 2025

The Goal

Sidreal Engine 2025 is a ground-up C++20 rendering/game engine designed as a hobby and experiment project to learn more about graphics programming. It emphasizes modern OpenGL practices: indirect drawing, bindless textures, SSBO-driven data, paired with a lightweight custom ECS for game logic. The engine is split into two compilation targets: a static Engine library and an App executable. This layer-based architecture cleanly separates engine code from game code, allowing multiple applications to share the same engine binary.

This project is now on hold as now I’m building my ongoing Sidreal 2.0 - Vulkan Edition project (I will link to it here soon), where I switch my entire rendering backend to Vulkan using it’s most recent render APIs for even more performance, features and granular control over my rendering.

What I Built

  • GPU-driven rendering pipeline with indirect draws scaling to 100,000+ instances
  • Custom binary glTF model loading with cached asset registry
  • Custom ECS with type-erased component storage and free-list entity recycling
  • Bindless-texture material lookup via GL_ARB_bindless_texture with zero texture binding state changes
  • Mesh deduplication via hash-based flyweight pattern
  • Directional-light shadow mapping with a dedicated 2048×2048 depth pass
  • Fly and orbital camera systems with quaternion-based rotation
  • Hot shader reloading: press R and iterate without restarting
  • Debug renderer for wireframe boxes and line segments
  • Premake5 build system with Debug, Release, and Dist configurations

Rendering Architecture

GPU Memory Layout

The entire renderer is SSBO-centric. All vertex and index data lives in Shader Storage Buffer Objects, and draw calls are batched via glMultiDrawElementsIndirect.

BindingBufferCapacityPurpose
0Vertex SSBO1M vertices × 48 bytesAll vertex data
1Index SSBO3M indices × 4 bytesAll index data
2Model Matrix SSBO100K instances × 64 bytesPer-instance transforms
3Texture Handle SSBO100K instances × 8 bytesARB bindless texture handles
4Texture Index SSBO100K instances × 4 bytesTexture sampler indices
5Debug Line SSBOVariableDebug wireframe lines

Vertex Pulling & GPU-Driven Draws

The default vertex shader pulls its own input with gl_VertexID, then uses gl_BaseInstance and gl_InstanceID to address instance data directly from SSBOs. Mesh work is submitted with glMultiDrawElementsIndirect, batching indexed draw commands from GPU-resident data rather than making a separate CPU-side draw call per mesh.

Bindless Textures

Using GL_ARB_bindless_texture eliminates texture binding state changes between draw calls. Each texture gets a 64-bit handle stored in an SSBO, and the fragment shader indexes into it, removing texture unit management and MAX_TEXTURE_IMAGE_UNITS constraints entirely.

Mesh Registration & Deduplication

Mesh registration hashes vertex data for deduplication: if a hash matches an existing mesh, the mesh ID is reused (flyweight pattern). New meshes upload vertices and indices to SSBOs and create DrawElementsIndirectCommand structures. Built-in primitives (CreateCube, CreateQuad) are available for debug rendering and prototyping.

Shadow Mapping

A separate framebuffer with a 2048×2048 depth texture attachment. RenderShadowPass() renders all meshes from the light’s perspective using a dedicated shadow.vert / shadow.frag shader pair. The shadow map is passed as sampler2DShadow to the main fragment shader, which combines it with diffuse lighting using smoothstep.

Shader System

Namespace-based API with uniform location caching. CreateShaderProgram() compiles and links, while ReloadShaders() hot-reloads all cached shader programs from disk.

ProgramShadersPurpose
Defaultdefault.vert + default.fragMain scene: diffuse lighting, texturing, shadow mapping
Shadowshadow.vert + shadow.fragDepth-only pass for shadow map generation
Debugdebug.vert + debug.fragDebug line rendering

Debug Renderer

Frame-buffered debug line rendering with a separate SSBO and shader program. Supports drawing wireframe bounding boxes (DrawBox) and individual line segments (DrawLine), rendered through RenderDebug() with optional depth testing.

Entity Component System

A custom ECS with type-erased component storage and free-list entity ID recycling (up to 65,536 entities). Built from scratch rather than using a library, keeping full control over the architecture and the engine self-contained.

Components

ComponentPurpose
TransformPosition, rotation (quaternion), scale
CameraFOV, near/far planes, view and projection matrices
MeshRendererMesh ID, instance index, AABB
HierarchyNodeParent reference, children array (32 max)
StageTagStage membership

Systems

  • RenderSystem: Iterates MeshRenderer entities, computes model matrices from Transform + parent hierarchy, updates GPU buffers, and triggers render
  • FlyCameraSystem: FPS-style camera with WASD movement, mouse look via quaternion composition, Alt toggles mouse lock
  • OrbitalCameraSystem: Orbit camera with scroll-to-zoom, right-click rotate, middle-click pan, and LERP smoothing

Architecture

Layer-Based Application

The engine owns the window, input, renderer, and ECS. Application layers inherit from ApplicationLayer and implement lifecycle hooks:

  • OnStart(): Initialize assets, create entities, attach components, set up camera
  • OnUpdate(): Run ECS systems, handle input-driven logic
  • OnRender(): Trigger renderer, draw debug overlays, render ImGui

Core Systems

SystemPurpose
LoggerHeader-only variadic logging with Windows console color support
WindowGLFW abstraction with multi-window support and lifecycle hooks
InputPer-window input state with edge-triggered (fire-once) and continuous detection
StageLogical scene grouping with object pool and free-list ID recycling
EngineMain loop orchestrator: poll events, update windows, render, present

Asset Pipeline

Asset Registry

Flyweight pattern for asset caching with static modelCache and textureCache. Cache-first lookup before attempting disk load, with support for loading from file path or raw memory (for embedded GLB textures).

Binary glTF Loader

Custom .glb parser that validates the GLB header, parses the JSON chunk (accessors, buffer views, buffers, meshes, materials, textures), extracts binary chunk data, and reads POSITION, NORMAL, TEXCOORD_0 attributes alongside index buffers and embedded texture data. Returns vector<MeshData> for each mesh primitive.

Image Loader

stb_image wrapper supporting both file-based and memory-based image loading (PNG, JPEG, etc.).

Dependencies

LibraryPurposeIntegration
GLFWWindowing, input, V-SyncStatic library, built from source
GLADOpenGL function loaderHeader + source, included in Engine
GLMMathematics (vectors, matrices, quaternions)Header-only
ImGuiDebug UI / IMGUI overlaySource files, OpenGL3 + GLFW backend
MathFuExtended math utilities (ray casting, geometry)Header-only
stb_imageImage loadingHeader-only
nlohmann/jsonJSON parsing (GLTF metadata)Header-only