Band above the prompt showing context-window fill and prompt-cache time left as bars

A C++20 game engine built on top of NVRHI with Vulkan as the primary graphics back-end.
msdf-atlas-genScriptableEntity behaviours and a SceneManager for multi-scene games; supports both 2D and 3D entities (ECS backend kept internal)RigidBody2D / BoxCollider2D / CircleCollider2D components, gravity, forces/impulses; physics backend kept internal)Physics3D, usable standalone or wired into the ECS (Transform3D / MeshRenderer / RigidBody3D / Box+SphereCollider3D components), with 3D meshes drawn through Renderer3D and a perspective camera (physics backend kept internal)Renderer3D with coloured directional, point and spot lights (up to 32 point/spot lights per scene, the most relevant picked each frame), ambient light, Blinn-Phong specular, and lit materials with emissive and an albedo texture. Lights are ECS components (PointLightComponent / SpotLightComponent / AmbientLightComponent / DirectionalLightComponent), and the lit shader hot-reloads in Debug builds when asset hot-reload is enabledEntity::SetParent), world values computed per pass, subtree destroy/duplicate, and physics that follows parentsScene::GetLightVisibility)Soft curve that leaves everything below 0.8 untouched, ACES, Khronos Neutral), bloom and ambient occlusion, per camera through PostProcessComponent--profileAnimator with cross-fades, Blend1D blends, masked layers and one-shots; clip events (code or a .events file beside the model) delivered to scripts; joint sockets; retargeting by joint name; models and their events hot-reload in place; an F7 Animation tabUsage guides for building games with the engine live in docs/:
.lib + headers, and a minimal appPhysics3D, standalone or ECS-integrated, including mesh collidersAssetManager, UUID handles, async loading, and hot-reloadCurrently at v0.8.3, which closes every open bug issue, from animation event ranges to Vulkan frame ordering, and adds what Headstone asked for: text that turns and keeps its z, scenes rendered into textures, saving textures as PNGs, and gamepad rumble. Before it, v0.8.2 let DirectX 12 start on a machine without a GPU and v0.8.1 fixed back-face culling for custom materials. All three sit on v0.8.0: the animation engine work (skinned models, an animator with blending, layers and timeline events, joint sockets, and in-place model hot-reload) and its example game, Marionette. Before it, v0.7.0 shipped the lighting engine and Candlewick, v0.7.1 the transform hierarchy, and v0.7.2 a choice to keep updating in the background (UpdateInBackground). Every milestone ships with an example game that exercises it — see ROADMAP.md for the full plan, the point releases (v0.4.1–v0.4.3, v0.5.1, v0.6.1–v0.6.3, v0.7.1–v0.7.2, v0.8.1–v0.8.3) and what each example is built to demonstrate.
| Version | Milestone | Example game | Status |
|---|---|---|---|
| v0.1 | Core Foundation — windowing, input, basic rendering pipeline | FlappyBird | shipped |
| v0.2 | Extended Rendering Pipeline — D3D11/D3D12 back-ends, Renderer3D, render thread | Breakout3D | shipped |
| v0.3 | Scenes & ECS | SpaceInvaders | shipped |
| v0.4 | Physics & Collision — Box2D 2D and Jolt 3D, then 3D inside the ECS | AngryBirds, DungeonCrawler3D | shipped |
| v0.5 | Audio & Gameplay-Grade Physics — miniaudio, character controller, ray/shape casts | EchoVault | shipped |
| v0.6 | Asset Pipeline & Hot-Reload — AssetManager, async loading, live reload | ArenaShooter | shipped |
| v0.7 | Lighting & Shading — point/spot lights on a capped forward multi-light path, specular | Candlewick | shipped |
| v0.7.1 | Transform Hierarchy — parent-child transforms in 3D and 2D, world-space rendering, lights, audio and physics | DungeonCrawler3D, EchoVault | shipped |
| v0.8 | Animation & Character Fidelity — GPU-skinned meshes, clips, blending and layers, timeline events, joint sockets | Marionette | shipped |
| v0.9 | Shadows, Post-processing & VFX — cascaded and local shadows, tone mapping, bloom, AO, GPU particles | Candlewick, Marionette upgrades | shipped |
| v1.0 | Stability, Performance & Polish — docs, Linux validation, culling + instancing | Dungeon Crawler (full release) | planned |
Shipped out of band: scripting (C# or Lua) and networking/multiplayer are optional modules layered onto a released engine rather than numbered milestones — neither blocks the version train, and no single-player game carries a transport layer it never uses. Online co-op is therefore not part of the 1.0 launch.
Prerequisites
VULKAN_SDK environment variable set (on Linux, LunarG's tarball and its setup-env.sh; distro packages are too old)1. Clone the repository
git clone --recursive https://github.com/KingoBoiii/DingoEngine.git
If cloned non-recursively, fetch submodules afterwards:
git submodule update --init
2. Generate project files
On Windows, run Generate-Windows.bat from the root directory. This will invoke Premake5 and produce a Visual Studio solution with all projects and dependencies configured.
On Linux, first build assimp 6.0.4 as a static library into vendor/assimp/lib/linux-x86_64/ (see Getting Started), then run Generate-Linux.sh, which runs the repo's premake (vendor/premake/bin/premake5 gmake). VS Code users get Linux build tasks and gdb launch entries in .vscode.
3. Build & run
On Windows, open the generated DingoEngine.slnx in Visual Studio, set one of the example projects (FlappyBird, Breakout3D, DungeonCrawler, SpaceInvaders, AngryBirds, DungeonCrawler3D, EchoVault, ArenaShooter, Candlewick, or Marionette) as the startup project, and build.
On Linux, run make -j"$(nproc)" config=debug (or release, distribution, debug-asan; add a project name to build just that one), then start an example from its own directory so its assets/ resolve:
cd examples/FlappyBird && ../../build/bin/Debug-linux-x86_64/FlappyBird/FlappyBird
Linux has been verified headless on Mesa's software Vulkan driver; real GPUs and desktops are still being validated (see LINUX-SUPPORT.md).
| Project | Description |
|---|---|
FlappyBird | Complete 2D game — sprites, input, collision, audio, score rendering |
Breakout3D | 3D Breakout — perspective camera, mesh rendering, manual AABB collision |
DungeonCrawler | Top-down 2D slice — tile collision, chasing enemies, melee combat, loot |
SpaceInvaders | Scene/ECS showcase — EnTT entities and a multi-scene SceneManager |
AngryBirds | 2D physics showcase — slingshot launching, destructible block towers, and pig targets on the Box2D-backed physics world |
DungeonCrawler3D | 3D dungeon-crawler prototype — the first ECS-integrated 3D scene: procedurally generated dungeons (rooms + corridors), player/enemies/walls as RigidBody3D entities on the Jolt-backed Physics3D, melee combat (SPACE) with enemy health + a player health bar, treasure to collect, a follow camera, drawn via Renderer3D; run with --night for a dark dungeon lit by a lantern and point-lit treasure |
EchoVault | v0.5 showcase — capsule character controller on floating platforms (slopes, stairs, moving kinematic platforms), ray/shape-cast gameplay (patrolling sentry line-of-sight), and 3D positional audio you navigate by, with full gamepad play; since v0.7 its orbs and sentries are lit emissive materials that carry point lights |
ArenaShooter | v0.6 showcase — wave-based top-down shooter driven entirely by the AssetManager: async loading behind a progress bar, all sprites/audio/fonts via UUID handles, and live hot-reload (edit assets/shaders/background.glsl or a sprite PNG while it runs) |
Candlewick | v0.7 showcase — a stealth crawl through a dark keep where every light is a gameplay object: the lantern you carry is a point light whose radius is your oil, wardens carry point lights and see through spot-light vision cones tested with the renderer's own light query (GetLightAttenuation), so the cone drawn on the floor is the cone that catches you, a game-side light budget keeps the decorative flames and the gameplay lights inside the engine's 32, and lit emissive braziers (hold to light) are the checkpoints; light the Chapel altar to win. Move with WASD / arrows / left stick, snuff or relight the lantern with Q / (X), hold E / (A) beside a brazier, pause with Esc / Start; synthesised 3D positional audio, full gamepad play, and --debug-cone to draw what the wardens test. The v0.9 upgrade: every gameplay light casts shadows you can hide in (wardens and braziers ask the renderer's own shadows through shadow probes), tone mapping and bloom on the flames, particle flames, embers and smoke, and AO |
Marionette | v0.8 showcase — a melee duel against three escalating opponents where no combat timing lives in code: every wind-up, hit window, combo window and dodge's invulnerability is a timeline event in a .events file beside the clips, swords and hit spheres ride joint sockets, a masked upper-body layer blocks while the legs keep walking, a Blend1D locomotion blend feeds positional footsteps from step events, and one set of clip libraries is retargeted onto four characters. Three AI tiers read the opponent's wind-up through a reaction delay and parry, block or dodge it; edit a .events file while it runs (--hot-reload, or --live-edit-demo) and the fight changes live. Move with WASD / left stick, light attack J / left mouse / X, heavy K / right mouse / Y, hold Shift / right bumper to block (tap to parry), dodge with Space / A, pause with Esc / Start; --debug-hitbox draws the hit and hurt spheres, --check runs its built-in checks, and --autoplay or --tournament=N play AI against AI. The v0.9 upgrade: footfall dust and blade trails fired straight from the clips' events (ParticleEventComponent), sparks where a blade lands, a parry flash that blooms, knock-out dust, particle braziers, and a moon and braziers casting the fighters' skinned shadows, with tone mapping and AO |
include/DingoEngine/ Public API
src/DingoEngine/ Engine implementation
vendor/ Third-party dependencies (submodules)
examples/ Example projects
| Library | Purpose |
|---|---|
| NVRHI | Graphics API abstraction (Vulkan / D3D12) |
| GLFW | Windowing and input |
| GLM | Math (vectors, matrices, quaternions) |
| spdlog | Logging |
| Dear ImGui | Debug UI |
| stb | Image loading |
| msdf-atlas-gen | Font SDF atlas generation |
| EnTT | Entity-component system (scenes) |
| Box2D | 2D rigid-body physics simulation |
| Jolt Physics | 3D rigid-body physics simulation |
*Marionette*'s characters, weapons and animations: KayKit by Kay Lousberg, www.kaylousberg.com — CC0. Each pack's licence text is kept in examples/Marionette/assets/.
The test app's Fox (test/assets/models/Fox): model by PixelMannen, rigging and animation by @tomkranis, glTF conversion by @AsoboStudio with @scurest — CC-BY 4.0, from the Khronos glTF Sample Models.
hooks/register.tsx 211 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionContextUsage } from 'claude-code'
3
4import type { CacheReading, ContextReading } from '../types'
5
6const context = atom({ plugin: 'session-meter', key: 'context' } as const, null)
7const cache = atom({ plugin: 'session-meter', key: 'cache' } as const, null)
8const now = atom({ plugin: 'session-meter', key: 'now' } as const, 0)
9
10const TICK_MS = 15_000
11// Label and bar in fixed cells, shared with usage-meter, so the bars line up in any font.
12const LABEL_CELLS = 9
13const BAR_TRACK = '#3a3a3a'
14const HOUR_MS = 60 * 60_000
15const FIVE_MIN_MS = 5 * 60_000
16
17function toReading(c: SessionContextUsage): ContextReading {
18 return { tokens: c.tokens, window: c.window, percent: c.percent }
19}
20
21async function tick($: EngineInterface) {
22 try {
23 const t = await $.clock.now()
24 await update($, now, () => t)
25 } catch {
26 // A missed tick only delays the countdowns by one period.
27 }
28}
29
30function formatTokens(n: number): string {
31 if (n >= 1_000_000) {
32 return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`
33 }
34
35 return n >= 1_000 ? `${Math.round(n / 1_000)}k` : `${n}`
36}
37
38function formatDuration(ms: number): string {
39 const totalSeconds = Math.max(0, Math.ceil(ms / 1_000))
40 const totalMinutes = Math.floor(totalSeconds / 60)
41 const hours = Math.floor(totalMinutes / 60)
42
43 if (hours > 0) {
44 return `${hours}h ${totalMinutes % 60}m`
45 }
46
47 return totalMinutes >= 1 ? `${totalMinutes}m` : `${totalSeconds}s`
48}
49
50function levelColor(percent: number): string {
51 return percent >= 80 ? 'red' : percent >= 50 ? 'yellow' : 'green'
52}
53
54export const register: Register = (on, options) => {
55 const ttlMs = options.cacheTtl === '5m' ? FIVE_MIN_MS : HOUR_MS
56
57 on('session.start', async ($, e, next) => {
58 const started = await next(e)
59
60 const usage = await $.session.usage()
61 await update($, context, () => toReading(usage.context))
62
63 await tick($)
64 $.clock.every(TICK_MS, () => void tick($))
65
66 return started
67 })
68
69 // A resumed session (a restart included) picks up the cache where the transcript left it.
70 on('classic.SessionStart', async ($, e, next) => {
71 const seconds = e.seconds_since_last_response
72 if ((e.source === 'resume' || e.source === 'fork') && seconds !== undefined) {
73 const reading: CacheReading = {
74 lastAt: (await $.clock.now()) - seconds * 1_000,
75 readTokens: e.context_tokens ?? 0,
76 writeTokens: 0,
77 inputTokens: 0,
78 }
79 const current = await read($, cache)
80 if (current === null || current.lastAt < reading.lastAt) {
81 await update($, cache, () => reading)
82 }
83 }
84
85 return next(e)
86 })
87
88 // A /clear starts a new conversation: the old cache entry no longer applies.
89 on('session.end', async ($, e, next) => {
90 if (e.reason === 'clear') {
91 await update($, cache, () => null)
92 }
93
94 return next(e)
95 })
96
97 on('session.measure', async ($, e, next) => {
98 if (e.changed.includes('context')) {
99 await update($, context, () => toReading(e.context))
100 }
101
102 return next(e)
103 })
104
105 // Every main-thread API response restarts the cache's TTL.
106 on('turn.step', async function* ($, e, next) {
107 const result = yield* next(e)
108
109 if (!e.agentId && result.usage) {
110 const u = result.usage
111 const reading: CacheReading = {
112 lastAt: await $.clock.now(),
113 readTokens: u.cache_read_input_tokens,
114 writeTokens: u.cache_creation_input_tokens,
115 inputTokens: u.input_tokens,
116 }
117 await update($, cache, () => reading)
118 await update($, now, () => reading.lastAt)
119 }
120
121 return result
122 })
123
124 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
125 const below = await next(e)
126 if (e.props.hasSurvey) {
127 return below
128 }
129
130 const ctx = await read($, context)
131 const c = await read($, cache)
132 await read($, now) // subscribes the band to the countdown ticker
133 const t = await $.clock.now()
134 const { Box, Text } = $.ui.resolve(e)
135
136 const width = Math.max(10, Math.min(24, e.props.bodyColumns - 70))
137
138 const label = (text: string) => (
139 <Box width={LABEL_CELLS} flexShrink={0}>
140 <Text bold>{text}</Text>
141 </Box>
142 )
143
144 // Solid cells, not glyphs: a proportional font draws █ and ░ wider than a cell.
145 const bar = (percent: number, color: string) => {
146 const filled = Math.round((Math.max(0, Math.min(percent, 100)) / 100) * width)
147 const parts = []
148 if (filled > 0) {
149 parts.push(
150 <Box width={filled} flexShrink={0} backgroundColor={color}>
151 <Text>{' '.repeat(filled)}</Text>
152 </Box>,
153 )
154 }
155 if (filled < width) {
156 parts.push(
157 <Box width={width - filled} flexShrink={0} backgroundColor={BAR_TRACK}>
158 <Text>{' '.repeat(width - filled)}</Text>
159 </Box>,
160 )
161 }
162
163 return (
164 <Box width={width} flexShrink={0} overflow="hidden">
165 {parts}
166 </Box>
167 )
168 }
169
170 const pct = ctx?.percent
171 const ctxColor = pct === undefined ? 'gray' : levelColor(pct)
172 const ctxText =
173 ctx === null || pct === undefined
174 ? ' no reading yet'
175 : ` ${pct}% · ${formatTokens(ctx.tokens ?? 0)} / ${formatTokens(ctx.window)}`
176
177 const leftMs = c === null ? 0 : c.lastAt + ttlMs - t
178 const isLive = leftMs > 0
179 const cacheColor = !isLive ? 'red' : leftMs > 5 * 60_000 ? 'green' : 'yellow'
180 const cacheText = isLive
181 ? ` · cold in ${formatDuration(leftMs)}`
182 : c === null
183 ? ''
184 : ` · re-caches ~${formatTokens(c.readTokens + c.writeTokens)} next message`
185
186 const mine = (
187 <Box>
188 {label('context')}
189 {bar(pct ?? 0, ctxColor)}
190 <Text color={ctxColor} bold>
191 {ctxText}
192 </Text>
193 <Text dimColor> │ </Text>
194 <Text bold>cache </Text>
195 <Text color={cacheColor} bold>
196 {isLive ? '● live' : '○ cold'}
197 </Text>
198 <Text dimColor>{cacheText}</Text>
199 </Box>
200 )
201
202 // Stack above any band another plugin drew beneath this one.
203 return below.type === 'engine' ? mine : (
204 <Box flexDirection="column">
205 {mine}
206 {below}
207 </Box>
208 )
209 })
210}
211types/index.d.ts 30 lines1export type ContextReading = {
2 /** Input tokens the last response was answered over; absent before the first response. */
3 tokens?: number
4 /** The model's context window, in tokens. */
5 window: number
6 /** `tokens` over `window`, 0 to 100; absent before the first response. */
7 percent?: number
8}
9
10export type CacheReading = {
11 /** When the last main-thread response arrived, epoch ms. */
12 lastAt: number
13 /** Input tokens that response read from the prompt cache. */
14 readTokens: number
15 /** Input tokens that response wrote to the prompt cache. */
16 writeTokens: number
17 /** Uncached input tokens of that response. */
18 inputTokens: number
19}
20
21declare module 'claude-code' {
22 interface PluginState {
23 'session-meter': {
24 context: ContextReading | null
25 cache: CacheReading | null
26 now: number
27 }
28 }
29}
30