Imported from matthewgattis/material-engine (
AGENTS.md). Install upstream withnpx skills add matthewgattis/material-engine. Copyright stays with the author.
AGENTS.md
Project Overview
Material Engine is a reusable Vulkan engine library using C++23 with CMake and vcpkg. Three targets: glass-ecs (dependency-free ECS core, links only glm) -> steel (Vulkan RAII wrappers + OpenXR) -> glass (engine abstractions with event dispatch and rendering, links glass-ecs + steel). Headless consumers (game servers, tools) can link glass-ecs alone without pulling in Vulkan/SDL. Designed to be consumed as a git submodule by application projects. Always-on FXAA 3.11 anti-aliasing and Dear ImGui debug overlay are applied as post-processing passes on the desktop view inside steel::Engine.
Build
cmake --preset default
cmake --build build
cd build && ctest --output-on-failure
Requires: CMake 3.25+, C++23 compiler, Vulkan-capable GPU. All dependencies (including glslc via shaderc) managed by vcpkg.
As a Submodule
# Parent project finds packages, then:
add_subdirectory(material-engine)
target_link_libraries(my_app PRIVATE glass)
When added via add_subdirectory(), the parent project is responsible for calling find_package() for all required dependencies. Material Engine only calls find_package() when built standalone.
Code Organization
- Top-level CMakeLists.txt: Finds packages (standalone only) and adds subdirectories. Do not add targets here.
- glass-ecs/: Dependency-free ECS core (entity, component pools, world, views, scheduler, sim components). Namespace
glass. Links only against glm. Safe for headless/server use. - steel/: Vulkan RAII engine library. Namespace
steel. Links against Vulkan, GLM, SDL3, spdlog, imgui, VMA, OpenXR. - glass/: Engine abstraction layer. Namespace
glass. Links againststeel. Provides meshes, materials, ECS (Entity Component System), event dispatch, and rendering abstractions built on top of steel's Vulkan wrappers. - test/: Google Test suite. Links against
steel,glass, and GTest. Built whenMATERIAL_ENGINE_BUILD_TESTS=ON.
Each subdirectory has its own CMakeLists.txt.
Conventions
- C++ standard: C++23, no extensions
- Namespaces:
steelfor Vulkan RAII wrappers,glassfor engine abstractions - Vulkan: Use
vk::raii::types exclusively (RAII wrappers, no manual cleanup) - Headers:
<module>/include/<module>/layout (e.g.,steel/include/steel/engine.hpp) - Shaders: GLSL 450 in
steel/shaders/, compiled to SPIR-V byglslcat build time. Steel's internal FXAA shaders (fullscreen.vert,fxaa.frag) are compiled to SPIR-V and embedded asconstexprarrays in a generated header (not checked into git). - Front face: Default front face is clockwise (
vk::FrontFace::eClockwise) - Push constants: Used for per-object model transforms, pushed per draw call
- Descriptor sets: Set 0 = per-frame UBO (view + projection matrices), set 1 = reserved for per-material (future)
- Tests: No GPU required. Test struct layouts, type traits, Vulkan struct construction, and utilities.
- Vulkan HPP structs: Use member assignment or constructor syntax, not C++20 designated initializers (they do not work reliably with Vulkan HPP types)
Key Interfaces
steel::Engine
Engine(title)orEngine(EngineConfig)— creates window and initializes Vulkan. Auto-selects largest fitting 4:3 resolution from predefined list for the primary display.EngineConfigsupports extra Vulkan instance/device extensions, API version override, and a physical device query callback (used by OpenXR).- High-DPI support via
SDL_WINDOW_HIGH_PIXEL_DENSITY begin_frame()->const vk::raii::CommandBuffer*(nullptr if frame unavailable). Callsbegin_command_buffer()+begin_scene_pass(). Sets dynamic viewport and scissor from the current extent. Flushes deferred destruction queue.begin_command_buffer()->const vk::raii::CommandBuffer*— fence wait, swapchain acquire, begin command buffer (without starting scene render pass). Used by XR path.begin_scene_pass()— begins the offscreen scene render pass. Called separately in XR mode after XR eye rendering.end_frame()— submits and presents- FXAA 3.11 post-processing: the scene renders to an offscreen target, then an FXAA fullscreen pass (quality preset 12 with edge endpoint search) reads it via a combined image sampler descriptor and writes to the swapchain. The FXAA pipeline is built directly, separate from
PipelineBuilder. Thebegin_frame()/end_frame()API is unchanged — consumers are unaware of FXAA. wait_idle()— waits for device idle (used for clean shutdown)poll_events()->bool(false = quit requested). Handles quit, resize, and delta time. Forwards all other events via optional event callback.set_event_callback(fn)— single event callback slot, typically claimed byglass::EventDispatcherdelta_time()— frame delta in seconds, clamped to 0.1s maxcurrent_frame()— current frame-in-flight indexdefer_destroy<T>(resource)— type-erased deferred destruction, holds resource forMAX_FRAMES_IN_FLIGHT + 1frameswindow()->SDL_Window*- ImGui:
imgui_begin(),imgui_end(),imgui_enabled(),set_imgui_enabled(),imgui_process_event() - Frames in flight:
MAX_FRAMES_IN_FLIGHT(2, defined in engine.hpp) - Accessors:
instance(),device(),physical_device(),render_pass(),extent(),command_pool(),graphics_queue(),graphics_family(),color_format(),depth_format(),allocator()
steel::UniformBuffer<T>
- Header-only template encapsulating descriptor set layout, pool, per-frame-in-flight sets, buffers, and persistent mapping
create(engine, stages)— creates layout, pool, sets, buffers with persistent mappingupdate(frame_index, data)— memcpy to mapped bufferbind(cmd, layout, set_index, frame_index)— binds descriptor setlayout()->const vk::raii::DescriptorSetLayout&
steel::PipelineBuilder
- Constructor:
PipelineBuilder(device, vert_spirv, frag_spirv)— takes SPIR-V bytecode upfront - Fluent API for remaining state:
set_vertex_input(bindings, attrs),set_topology(),set_polygon_mode(),set_cull_mode(),set_depth_test() - Default front face is
eClockwise(matching Vulkan convention with Y-flipped projection) build(render_pass, layout)->vk::raii::Pipeline— viewport and scissor are dynamic state
steel::Buffer
Buffer::create_vertex_buffer(...)— staging upload to device-local vertex bufferBuffer::create_index_buffer(...)— staging upload to device-local index bufferBuffer::create(...)— general buffer creationmap(),unmap()— host-visible memory access
steel::XrSystem
- OpenXR integration for HMD stereo rendering via
XR_KHR_vulkan_enable - Static two-phase initialization:
query_requirements()runs before Vulkan setup (returns required extensions + physical device query), constructor runs afterVkDevicecreation query_requirements()->optional<XrVulkanRequirements>— creates XrInstance, queries HMD system, returns required Vulkan extensions. Returns nullopt if no HMD. Stores static XR state for constructor.query_physical_device(VkInstance)->VkPhysicalDevice— queries the GPU the XR runtime requireshas_pending_session()— true ifquery_requirements()found an HMD- Constructor takes Vulkan handles, creates XrSession, reference space (LOCAL/seated), per-eye swapchains, depth buffers, render pass, and framebuffers
poll_events()— handles session state transitionsactive()— true when session is runningwait_and_begin_frame(body_position, body_yaw)->XrFrameState— xrWaitFrame, xrBeginFrame, xrLocateViewsbegin_eye_render(cmd, eye)/end_eye_render(cmd, eye)— per-eye render pass managementend_frame(XrFrameState)— xrEndFrame with projection layer- Coordinate transform: OpenXR Y-up -> engine Z-up via -90 deg X rotation
glass::EventDispatcher
EventDispatcher(engine)— registers assteel::Engine's sole event callback, fans out SDL events to multiple subscriberssubscribe(callback)->Subscription— RAII subscription handle; dropping it unsubscribes automatically- Callback signature:
void(const SDL_Event&, bool& handled)
glass::Camera
- Projection-only:
Camera(fov_degrees, aspect_ratio, near_plane, far_plane) set_aspect_ratio(float)— updated by renderer each frameprojection()->const glm::mat4&— Y-flipped for Vulkan- View matrix is derived from
glm::inverse(Transform.matrix)in the renderer
glass::Renderer
Renderer(engine)— creates per-frame UBO with separate view and projection matricesbind_world(world)— stores the world pointer for XR head-entity management; GPU cleanup is handled by GeometryCache handle deleters, not renderer subscriptionsset_camera(entity)— sets the active camera entityrender_frame(world)— desktop rendering pathrender_xr_eyes(cmd, world, frame_index, frame_state, xr)— stereo XR renderingrender_desktop_companion(cmd, world, frame_index, xr_view)— desktop companion view for XR modeframe_descriptor_layout()— exposes UBO descriptor set layout for material pipeline creationFrameUBO—mat4 view+mat4 projection
glass::Components
- Sim components (glass-ecs,
glass/transform.hpp):Transform—glm::mat4 matrix(default identity);Velocity—glm::vec3 linear(default zero) - Render components (glass,
glass/components.hpp, which re-exports the sim components):GeometryComponent—std::shared_ptr<const Geometry>(shared handle fromGeometryCache; deleter defers GPU destruction);MaterialComponent—const Material*(non-owning);CameraComponent—Camera camera
glass::Entity, World, View (glass-ecs)
Entity— lightweight handle:uint32_t index+uint32_t generationWorld— entity manager with create/destroy, component operations (add,remove,get,has),view<Ts...>()for multi-component queries- Signals:
on_construct<T>(fn)/on_destroy<T>(fn)fire(Entity, T&)with the component intact on add and on every removal path;on_entity_destroy(fn)listeners fire(World&, Entity)before any component is removed. All support multiple subscribers. - Change tracking:
patch<T>(e)returnsT&and advances the component's monotonic write version;version<T>(e)reads it; plainget()is untracked View<Ts...>— iterates smallest pool, filters by all requested types,each(fn)callback
glass::Phase, Scheduler (glass-ecs)
Phase—PreTick/Tick/PostTick(fixed-rate sim) andPreRender/Render(per displayed frame)Scheduler—add(phase, fn)registers avoid(World&, float dt)callable;run(phase, world, dt)invokes that phase's systems in registration order (deterministic)
glass::Material
Material::create(engine, vertex_shader, fragment_shader, frame_descriptor_layout)— pipeline layout includes descriptor set layout at set 0 and push constant range for model matrixbind(cmd),layout()— pipeline binding and layout access
glass::Mesh, Geometry, Shader, Vertex
Mesh— abstract interface:vertices(),indices()Geometry::create(engine, mesh)— uploads mesh to GPU via staging buffers.bind(cmd),draw(cmd).GeometryCache—get(key, mesh)uploads once per key and returnsstd::shared_ptr<const Geometry>handles whose deleter routes destruction throughengine.defer_destroy();erase(key)drops the cache reference (outstanding handles stay valid). Handles must not outlive the engine.Shader::load(stage, spirv_path)— loads SPIR-V from fileVertex—vec3 position+vec3 normal+vec3 color(36 bytes)
Adding New Code
- New steel features: add files under
steel/src/andsteel/include/steel/, updatesteel/CMakeLists.txt - New glass features: add files under
glass/src/andglass/include/glass/, updateglass/CMakeLists.txt - New ECS features: add files under
glass-ecs/include/glass/(andglass-ecs/src/), updateglass-ecs/CMakeLists.txt. Keepglass-ecsfree of steel/SDL/Vulkan includes. - New tests: add
.cppfiles undertest/, updatetest/CMakeLists.txt - New engine shaders: add
.vert/.fragundersteel/shaders/, updatesteel/CMakeLists.txtto compile and embed them - New dependencies: add to
vcpkg.json,find_package()in top-level CMakeLists.txt (guarded by standalone check), link in the appropriate subdirectory. Parent projects consuming material-engine as a submodule must also add the dependency to their ownvcpkg.json— material-engine's manifest is only used for standalone builds.