Step 7

C++ as an Engineering Tool

Build systems that scale, perform well, and make their costs visible.

Abstract

Simulation and graphics put constant pressure on software, but the deeper challenge is building systems that remain understandable as they grow. In that context, C++ is more than a language for math and memory. It is a language for engineering: organizing modules, managing resources, defining boundaries, and keeping behavior predictable under load.

This article introduces the engineering mindset behind real-time engines. We will take the Particle Playground—already capable of simulation and rendering—and reshape it into a modular, maintainable, RAII-driven system.

At this point, the Playground is no longer just a demo. It is beginning to resemble a small engine, and the next step is to make that structure intentional.

1. Why Engineering Matters in C++

C++ gives you a great deal of control, but that control needs structure:

  • raw pointers everywhere
  • global state
  • resource leaks
  • tangled modules
  • unpredictable lifetimes
  • fragile initialization sequences

Real time engines address this by relying on:

  • RAII
  • handles
  • module boundaries
  • explicit ownership
  • target based build systems
  • fail soft error handling

We will explore those patterns by evolving the Playground step by step, beginning with the most important idea: tying resource ownership to object lifetime.

2. RAII — The Foundation of C++ Engineering

RAII (Resource Acquisition Is Initialization) is the idea that:

If you own a resource, your object’s lifetime manages it.

This applies to:

  • GPU buffers
  • shader programs
  • windows
  • input contexts
  • timers
  • file handles
  • memory blocks

Instead of:

particle* particles = new particle[1000];  // manual ownership: easy to leak if cleanup is missed
delete [] particles;    // cleanup must be paired explicitly

A better approach is to wrap that ownership in a small template class that manages a fixed array of objects:

template <typename T>
class ppg_array {
    T* m_data;
    size_t m_size;
public:
    ppg_array() : m_data(nullptr), m_size(0) {}
    ppg_array(size_t size) : m_data(new T[size]), m_size(size) {}
    ~ppg_array() { delete[] m_data; } // releases owned storage automatically
    void resize(size_t new_size) {
        delete[] m_data;
        m_data = new T[new_size];
        m_size = new_size;
    }
    size_t size() const { return m_size; }
    T& operator[](size_t index) { return m_data[index]; }
    const T& operator[](size_t index) const { return m_data[index]; }
    T* data() { return m_data; }
    const T* data() const { return m_data; }
    // provide begin() and end() methods for range-based for loops
    T* begin() { return m_data; }
    T* end() { return m_data + m_size; } // one-past-the-end pointer used by range-based iteration
    const T* begin() const { return m_data; }
    const T* end() const { return m_data + m_size; }
};

The usage becomes straightforward:

ppg_array  particles(1000);  // storage is owned by the array object 

We no longer need to call delete[] manually. The array storage is released automatically when the particles object is destroyed at the end of its scope.

The Playground uses a slightly different layout in practice, one that better supports GPU updates while the application runs: particle positions, velocities, and colors are stored in separate arrays.

struct particle_system {
    ppg_array positions;
    ppg_array velocities;
    ppg_array colors;
};

RAII helps provide:

  • deterministic cleanup for owned resources
  • fewer missed release paths
  • less reliance on global shutdown functions
  • fewer ownership questions during debugging

RAII therefore becomes more than a convenience; it becomes a core mechanism for reliable engine architecture. Once lifetimes are controlled, the next question is what the rest of the code is allowed to see.

3. Handle-Based Architecture

Passing raw OpenGL object names throughout the codebase can quickly become difficult to manage:

  • they can leak when deletion paths are missed
  • they expose implementation details
  • they encourage global state
  • they make lifetime unclear

A common alternative is to expose opaque handles at module boundaries:

using Handle  = uint32_t;

Handles:

  • hide implementation
  • allow indirection
  • support pooling
  • enable debugging
  • support resource replacement and hot reload
  • make resource management uniform

A Resource Management System can then map handles to actual RAII objects, such as:

Buffer&  getBuffer(BufferHandle);
Program& getProgram(ProgramHandle);

This gives the architecture room to scale. Handles, however, work best when the codebase is divided into clear modules with well-defined responsibilities.

4. Module Boundaries — The Engine’s Skeleton

For the Playground, we can split the engine into the following modules:

core

Minimal utilities:

  • timer (implemented in btm-framework)
  • ppg_array
  • handle management in ppg_handle_manager

Together, these utilities give the project its foundational building blocks.

math

All math types:

  • vector2, vector3, vector4
  • matrix4
  • transform
  • camera (coming from btm-framework)
  • shaders (coming from btm-framework)

Pure, header only, and dependency free.

simulation

Pure CPU simulation:

  • particle
  • particle system
  • step_simulation driven by elapsed frame time

It does not depend on graphics, platform code, or OpenGL.

graphics

GPU side rendering:

  • RAII wrappers for buffers, shaders, programs
  • particle_renderer
  • handle based resource management

This module depends on math and core, but not on simulation.

platform

Window + context:

  • Window
  • Input
  • Platform abstraction
  • our own backend (btm-framework)

Because this module has no dependencies on the others, it completes a structure that keeps the engine understandable before additional runtime behavior is introduced.

5. Error Handling — Fail Soft, Not Hard

Unhandled exceptions can be a poor fit for real time loops:

  • they can unwind across latency sensitive code paths
  • they may add binary, ABI, or platform constraints
  • they complicate control flow

For this reason, many engines prefer explicit error channels:

Result<ProgramHandle> createProgram(...);

Where:

  • ok == true → valid handle
  • ok == false → diagnostic error information

The result is explicit, predictable control flow that supports the broader goal of making ownership and responsibility visible.

6. Resource Lifetime — The Engine’s Contract

Every module must define:

  • who owns what
  • who cleans up
  • who depends on whom

Examples:

  • Renderer owns GPU buffers and shader programs
  • Platform owns the window and context
  • Simulation owns particle data
  • Camera owns its view/projection state
  • Resource Manager owns all GPU resources

Modules should not depend on another module’s internal details.

That contract is what helps engines remain maintainable over time, and it prepares the full Playground structure to bring these ideas together.

7. The Particle Playground — Engine Edition

In Article 3, the Playground moves from a working demo toward a compact modular engine architecture.

New structure
theparticleplayground_3/
    core/
    math/
    graphics/
    platform/
New main loop
// Create the platform window and rendering context.
platform ppg_platform;
btm_window_handle pFrame = ppg_platform.create_window(800, 600, "The Particle Playground - part 3");
// std::cout << "Article 3 - C++ as an Engineering Tool" << std::endl;
// std::cout << "Article 4 - Patterns for Simulation and Graphics" << std::endl;

// Initialize simulation and rendering systems.
simulation sim(1000);
particle_renderer g_particle_renderer;
g_particle_renderer.init(sim.positions(), sim.colors());

// Configure the camera used for rendering.
btm::gl_camera camera(btm::fvec3(0, 0, 20), btm::fvec3(0, 0, 0), btm::fvec3(0, 1, 0));
camera.set_fov(btm::dtr(15.f));

btm::start_timer();
btm::get_elapsed_time();

// run the program loop
while (ppg_platform.application_active()) {
    float fElapsed = (float)btm::get_elapsed_time();
    sim.step_simulation(fElapsed);
    render(camera, g_particle_renderer, sim.positions());
}
stop_timer();

return 0;

The resulting loop is:

  • clean
  • modular
  • predictable
  • scalable

At this stage, the Playground looks less like a one-off sample and more like a small engine whose architecture can continue to grow.

8. Why This Architecture Matters

This modular, RAII driven, handle based architecture enables:

  • future features (instancing, batching, compute shaders)
  • debugging tools (resource tracking, hot reload)
  • platform abstraction (custom backends)
  • scalability (multiple render passes, multiple systems)
  • clean separation (simulation independent from rendering)

It mirrors production-engine patterns in a smaller, more approachable form, while preparing the project for the next article’s focus on data flow and integration.

9. Preparing for Article 4

With the engineering foundations in place, Article 4 will show:

  • how simulation and graphics form a pipeline
  • how data flows through the engine
  • how batching and transforms work
  • how to build a simple component like structure
  • how to integrate everything into a cohesive system

Article 3 establishes the structure. Article 4 builds on that foundation with patterns that connect the pieces, allowing the engine to grow from separate modules into a coordinated runtime system.

Closing Thoughts

C++ is not only a language for performance; it is also a language for architecture. RAII, handles, module boundaries, and explicit ownership give us practical techniques for building reliable real-time engine systems.

This article established the engineering mindset:

  • RAII as the foundation
  • handles as the interface
  • modules as the structure
  • resource managers as the backbone
  • target based CMake as the build system
  • explicit error handling as the safety net

With these tools in place, the Particle Playground becomes more than a collection of rendering and simulation code. It becomes a clean, compact engine foundation, ready for the pipeline, batching, and integration work that comes next.