Architecture
This page describes how libDQG’s pieces fit together. For the full API surface, see the API reference; this page is the map, not the territory.
Workspace layout
The workspace root’s Cargo.toml defines three crates:
core/— thelibdqglibrary crate (the framework itself).demo/— ademobinary crate exercising the framework; more an integration test and sample game than a real product.editor/— aneditorbinary crate: a visual scene editor built onlibdqg(anegui-based UI, its own project file format, mesh-accurate picking, and a.rhaiscripting system).
libdqg re-exports glam (pub use glam;) so downstream crates don’t
need to pin their own version to stay compatible with types like Sprite::transform.
Game loop: App → SceneManager → Scene
game.rs—GameBuilderconfigures window title/size/titlebar/clear color and builds aGame, which owns awinit::EventLoopand runs it againstApp.app.rs—Appimplementswinit::ApplicationHandler. It owns theWindow, theRenderer,InputState, and theSceneManager, and drives the frame loop:RedrawRequested→SceneManager::update→Renderer::render(clear_color, |pass| { scene.render(pass) }). It also handles custom window-chrome logic (drag-resize borders, integrated titlebar hit-testing) whenintegrated_titlebaris enabled, since window decorations are turned off in that mode.scene.rs— user code implements theScenetrait (update+render);updatereturns aSceneTransition(Push/Pop/Replace/Next/Previous/Quit/None) thatSceneManageracts on. Scenes are a flatVecwith a current index, not a stack machine with separate push/pop semantics for render vs. update.- Resource loading (textures, models) happens lazily inside
Scene::update, gated onrenderer: Option<&mut Renderer>beingSome. The renderer is only available once the window/GPU surface exists (afterresumed), so scenes must tolerateNoneon early frames.
Renderer: a thin wgpu wrapper + higher-level draw helpers
renderer/mod.rs—Rendererowns the wgpu device/queue/surface, the camera uniform buffer, the depth buffer, and every built-in pipeline (immediate shapes, screen-space sprites, world-space sprites, models).Renderer::renderbegins the wgpu render pass (color + depth attachments) and hands aDrawPassinto a caller-supplied closure — this is the only place a frame is actually submitted.Rendereralso exposes a generic pipeline-building API (create_shader_module,create_render_pipeline,create_buffer,create_buffer_init) built on wrapper types inrenderer/types.rs(VertexFormat,PrimitiveState,DepthStencilState, etc.). These wrapper enums exist so downstream crates (likedemo) can describe custom pipelines without depending onwgpudirectly — every wrapper type has ato_wgpu()/from_wgpu()conversion. Any custom pipeline built this way that participates in the main pass must declare a depth-stencil state usingRenderer::DEPTH_FORMAT(Depth32Float) or it falls back to a no-op overlay depth state.renderer/draw_pass.rs—DrawPasswrapswgpu::RenderPasswith the same wrapper-type philosophy. Higher-level draw calls (draw_rect/draw_ellipse/draw_line/draw_ui_sprite/draw_world_sprite/draw_model) live inextras.rs(immediate shapes + sprites) andmodel.rs(OBJ models).- Two coordinate spaces coexist per frame: screen space (pixel coordinates, e.g.
draw_rect,Sprite::draw, the titlebar) uses depth-untested overlay rendering so draw order wins; world space (draw_world_sprite,draw_model, camera-relative) is depth-tested against the sharedDepth32Floatbuffer. - Shaders are plain
.wgslfiles undercore/shaders/, pulled in viainclude_str!.
Camera
Camera (camera.rs) is an FPS-style camera: position plus yaw/pitch (up is fixed to world
+Y, so there’s no roll), rather than an eye/target look-at pair. Camera::forward() derives the
look direction from yaw/pitch, and Camera::look_at(target) is a convenience for call sites that
think in terms of a point to look at rather than angles. Renderer owns exactly one Camera;
Renderer::render re-derives the GPU view-projection uniform from it every frame, so swapping
*renderer.camera_mut() = ... before calling render is the whole mechanism for changing what’s
displayed — there’s no separate multi-camera concept in the renderer itself. (The editor builds a
camera-entity concept in the ECS on top of this — see The editor below.)
Transforms
transform.rs defines Transformable, a default-method trait giving any type with a glam::Mat4
(Sprite, Model) a fluent transform API (translate/rotate/scale/flip_x/…). Rotations
and scales compose about Transformable::pivot() (defaults to local origin; Sprite overrides it
to its quad center so sprites spin in place). All operations post-multiply onto the existing
matrix in local space — chained calls read outside-in (e.g. rotate(...).translate(...)
translates along the rotated axis). set_transform/reset_transform bypass composition
entirely when you want to drive the matrix directly.
ECS: World, Entity, ComponentStore
core/src/ecs/ is a small hand-rolled ECS, not a library like hecs/bevy_ecs. Entity (index +
generation) is allocated/recycled by EntityAllocator; ComponentStore<T> is a sparse
Vec<Option<(generation, T)>> keyed by entity index, generation-checked on every access so a
stale Entity never reads/writes a recycled slot. There’s no query/archetype abstraction —
components are just named ComponentStore<T> fields on World.
world.rs’s World holds transforms, names, renderables, scripts, and cameras, each its
own ComponentStore. World::sync_transforms() copies each entity’s Transform into its
Renderable’s GPU-facing matrix once per frame. A CameraComponent (fov/near/far/active)
attached to an entity, combined with its Transform, derives a renderable Camera via
CameraComponent::derive_camera — this is what lets the editor place a camera in a scene (see
below) rather than only having the one built into Renderer.
The editor
The editor (editor/) is a scene editor built on libdqg. editor/src/main.rs is the entry
point; it builds a Game starting at a menu scene (New/Open/Recent project), which hands off to
EditorScene once a project is chosen. The UI is all egui, drawn across menu bar, hierarchy
panel, assets panel, and inspector.
- Project format — a
Projectis a folder on disk (a manifest,assets/{textures,models}/,scenes/main.ron,scripts/). Entities serialize as anEntityRecord(name/transform/renderable/camera/scripts); assets and scripts are referenced by project-root-relative path, never by an ID/handle. - Picking — mesh-accurate ray/triangle picking for models, ray/sphere for sprites and camera entities, driving hover/selection highlight in the viewport.
- Camera entities — an entity with a
CameraComponentrenders as a frustum gizmo in Edit mode and can be marked “active”; entering Play mode swaps the renderer’s camera to whichever entity is active, in place of the editor’s own fly camera. - Scripting — entities carry a stack of
.rhaiscript attachments (ScriptList/ScriptAttachmentincore/src/scripting.rs, aWorldcomponent like any other), created from the Assets panel and attached/reordered/removed from the Inspector. See Scripting for the full API scripts can use.ScriptRuntimecompiles/caches one RhaiASTper script path and runs each attachment’son_start()/on_update(dt, input)against a minimalentity/world/inputAPI — deliberately small; a script mostly only touches its own entity, plus limited cross-entity access viaworld.find(name).EditorScene’s Play/Stop toggle snapshots the wholeWorldbefore running scripts and restores it on Stop, dropping theScriptRuntime(and all script state) with it. A script that errors repeatedly auto-disables itself, and errors surface in a small overlay rather than crashing the editor.
Other core modules
camera.rs—Camera, described above.input.rs—InputState/MouseStatetrack per-frame key/button press/hold state and mouse position/wheel motion from winit events;Appclears frame-transient state after eachRedrawRequested.titlebar.rs/platform.rs— custom-drawn titlebar and Windows-specific chrome (rounded corners viawindows-sys) used whenintegrated_titlebar(true).util.rs—resolve_resource_path(exe-relative asset lookup fallback) andslice_to_bytes(raw byte view of a&[T]for uploading to GPU buffers).