Skip to content

Repository files navigation

ursus logo

URSUS

A Vulkan game engine written in Rust

Rust License: MPL--2.0 Vulkan Platform Lines of Code


📦 engine-core

A custom Vulkan game engine written in Rust, built around a render-graph architecture with automatic barrier tracking, a deferred rendering pipeline, and a threaded game/render split.

📁 Workspace layout

engine-core/       the engine itself: ECS, asset pipeline, Vulkan abstraction, render graph
engine-pipelines/    a batteries-included deferred renderer + built-in shaders, built on engine-core
engine-demo/   minimal example application showing how to use the engine

engine-core has no opinion about how you render things - it gives you the plumbing (device/swapchain setup, render graph, resource pool, asset loading, ECS). engine-pipelines is one opinionated pipeline built on top of that plumbing. You could write your own pipeline crate instead and skip engine-pipelines entirely.

⭐ Features

  • Render graph (engine-core/src/render/graph.rs) - passes declare read/write access to resources; the graph topologically sorts them and inserts image layout barriers automatically. See Render graph below.
  • Deferred pipeline (engine-pipelines) - shadow pass -> depth prepass -> GBuffer (albedo/normal) -> lighting -> tonemap/post-process -> FSR1 (EASU + RCAS) upscale -> UI overlay.
  • Bindless textures - a single descriptor set with update_after_bind + PARTIALLY_BOUND, indexed via nonuniformEXT in shaders. No per-draw descriptor churn.
  • Threaded renderer - game logic and rendering run on separate threads, synchronized via a lock-free triple buffer ( render/triple_buffer.rs). The render thread never blocks on game logic.
  • Async asset loading - a background thread loads .obj/.gltf/.glb off the hot path; results flow back through a channel and get GPU-uploaded when ready.
  • Text rendering - cosmic-text for shaping + a custom glyph atlas (etagere-backed packer) for rasterized text.
  • GPU profiling - per-pass timestamp queries (vulkan/timestamps.rs) feeding into puffin for frame-time breakdowns.

🕸️ Render graph

Passes are built declaratively and don't know about each other directly - dependencies are inferred from resource access:

pass("lighting")
.read(h_gbuffer_albedo, ImageLayout::ShaderReadOnly)
.read(h_gbuffer_normal, ImageLayout::ShaderReadOnly)
.read(h_depth, ImageLayout::ShaderReadOnly)
.read(h_shadow_map, ImageLayout::ShaderReadOnly)
.write(h_hdr, ImageLayout::ColorAttachment)
.bind_sampled(h_gbuffer_albedo, lighting_pass.descriptor_set, 0, lighting_pass.sampler)
.bind_sampled(h_gbuffer_normal, lighting_pass.descriptor_set, 1, lighting_pass.sampler)
.bind_sampled(h_depth, lighting_pass.descriptor_set, 2, lighting_pass.sampler)
.bind_sampled(h_shadow_map, lighting_pass.descriptor_set, 4, lighting_pass.shadow_sampler)
.record( move | enc, rw, gpu| lighting_pass.record(enc, rw, gpu, h_hdr))
.build(graph, & gpu_assets);

RenderGraph::compile() builds a dependency graph from these read/write accesses, topologically sorts the passes, and precomputes the minimal set of VkImageMemoryBarrier2s needed between them. Resources are either:

  • Transient - sized relative to internal or output resolution, allocated/resized by the graph (ResourcePool)
  • External - swapchain images, whose layout is managed at frame boundaries

Current pipeline order in engine-pipelines:

shadow ---------+
                |
                +--> geometry --> lighting --> post_process
                |                                  |
depth_prepass --+                                  v
                           fsr_easu --> fsr_rcas --> blit_to_swapchain --> ui

(depth_prepass and shadow have no dependency on each other and could run in parallel on hardware/APIs that support it - the graph doesn't currently exploit that, it just orders them consistently.)

🛠️ Building

Requires the Vulkan SDK installed with glslc on your PATH - shaders are compiled to SPIR-V at build time via build.rs (engine-pipelines/build.rs), there's no runtime shader compilation.

cargo build --release
cargo run -p engine-demo

If glslc isn't found, the build fails immediately with a clear panic rather than silently skipping shader compilation.

🎮 Example usage

See engine-demo/src/main.rs for a full example. Minimal shape:

struct MyApp;

impl App for MyApp {
    fn initial_pipeline() -> PipelineFactory { PipelineFactory::of::<LoadingPipeline>() }

    fn on_start(&mut self, ctx: &mut EngineContext) {
        ctx.world.spawn().insert(CameraComponent::default()).insert(ActiveCamera).build();
        ctx.world.spawn().insert(DirectionalLightComponent::default()).build();
    }

    fn on_update(&mut self, ctx: &mut EngineContext, dt: f32) { /* game logic */ }
    fn on_render(&mut self, _ctx: &mut EngineContext) {}
    fn on_stop(&mut self, _ctx: &mut EngineContext) {}
}

fn main() -> anyhow::Result<()> {
    Engine::run(MyApp)
}

Entities are plain hecs ECS entities (GameWorld wraps hecs::World). Each fixed tick, an ExtractSchedule copies relevant ECS state into a RenderWorld snapshot, which is published to the render thread via the triple buffer.

🚧 Status / known rough edges

  • Single discrete-GPU assumption in device selection (falls back to first suitable device if none found).
  • No parallelism exploited in the render graph yet - passes execute sequentially even when independent.
  • Some internal log/error strings are in Russian; not yet standardized to one language throughout.

🗺️ Roadmap

Engine architecture (engine-core):

  • Multithreaded command recording - record passes in parallel into secondary command buffers via a job system
  • Draw call batching / instancing - GPU instancing for identical meshes instead of one draw call per instance
  • Resource aliasing in the render graph - reuse memory across transient resources with non-overlapping lifetimes
  • Runtime debug UI - GPU timings, G-buffer view, live-tweaking of render parameters
  • vertex_format! proc-macro - derive VertexFormat (layout/offsets/stride) for custom vertex structs instead of a hand-written impl

Rendering backlog (engine-pipelines):

  • PBR lighting (specular/GGX, metallic)
  • Point light shadows
  • Cascaded shadow maps
  • IBL / ambient
  • Wire up frustum culling (already implemented in math/frustum.rs, currently unused)
  • Alpha blending pass

📄 License

This project is licensed under the Mozilla Public License 2.0 (MPL-2.0). See the LICENSE file for details.

About

A Vulkan game engine written in Rust providing a render graph, ECS, asset pipeline, and rendering abstractions for building custom rendering pipelines.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages