SLOPSHOPPER

session-meter

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

newbandtimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · session-meter
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM context 49% · 97k / 200k │ cache ○ cold ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
context 49% · 97k / 200k │ cache ○ cold
README

DingoEngine

A C++20 game engine built on top of NVRHI with Vulkan as the primary graphics back-end.

Features

  • Rendering — Vulkan-backed graphics pipeline via NVRHI; batched 2D renderer (quads, circles, MSDF text)
  • Windowing & Input — GLFW window management with a frame-coherent keyboard/mouse/gamepad input system (edge + held queries, mouse scroll/delta, up to 16 controllers with deadzone-filtered sticks)
  • Layer System — Stack-based middleware architecture for organizing application logic
  • Event System — Type-safe, decoupled event dispatcher (window, keyboard, mouse, gamepad)
  • ImGui Integration — Built-in debug/overlay UI layer with docking and viewport support
  • Font Rendering — MSDF atlas-based text rendering via msdf-atlas-gen
  • Asset Utilities — Texture, shader, and buffer creation with a fluent params/builder API
  • Scenes & ECS — entity-component scenes with ScriptableEntity behaviours and a SceneManager for multi-scene games; supports both 2D and 3D entities (ECS backend kept internal)
  • 2D Physics — Box2D-backed rigid-body simulation wired into the ECS (RigidBody2D / BoxCollider2D / CircleCollider2D components, gravity, forces/impulses; physics backend kept internal)
  • 3D Physics & Scene — Jolt-backed 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)
  • 3D Lighting — forward-lit 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 enabled
  • Transform Hierarchy — 3D and 2D parent-child transforms (Entity::SetParent), world values computed per pass, subtree destroy/duplicate, and physics that follows parents
  • Shadows (v0.9) — cascaded sun shadows, point and spot light shadows on a budget of slots, skinned casters, and gameplay shadow queries answered by the renderer's own lookup (Scene::GetLightVisibility)
  • Post-processing (v0.9) — an HDR scene target with tone mapping (a Soft curve that leaves everything below 0.8 untouched, ACES, Khronos Neutral), bloom and ambient occlusion, per camera through PostProcessComponent
  • GPU Particles (v0.9) — compute-simulated emitters with shapes, forces, curl noise, colour over life, sprites and soft edges, as ECS components, fired straight from animation events; a live effect editor in F4
  • Compute & Profiling (v0.9) — compute shaders and storage buffers, GPU pass timers in an F8 tab, and Tracy behind premake's --profile
  • Skeletal Animation — skinned glTF/FBX models drawn with GPU skinning; an Animator 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 tab

Documentation

Usage guides for building games with the engine live in docs/:

  • Getting Started — prerequisites, linking the .lib + headers, and a minimal app
  • Application & Layers — entry point, lifecycle, input, events
  • 2D Rendering — quads, circles, text, textures, fonts
  • Scenes & ECS — entities, components, systems, and scene management
  • 2D Physics — rigid bodies, colliders, gravity, and forces/impulses
  • 3D Physics — the Jolt-backed Physics3D, standalone or ECS-integrated, including mesh colliders
  • Asset Pipeline — the AssetManager, UUID handles, async loading, and hot-reload
  • Lighting — directional, point and spot lights, the light budget, specular, and lit materials
  • Animation — skinned models, the animator, blending and layers, timeline events, joint sockets, and model hot-reload
  • Shadows, Post-processing, Particles, Compute and Profiling — the v0.9 visuals

Roadmap

Currently 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.

VersionMilestoneExample gameStatus
v0.1Core Foundation — windowing, input, basic rendering pipelineFlappyBirdshipped
v0.2Extended Rendering Pipeline — D3D11/D3D12 back-ends, Renderer3D, render threadBreakout3Dshipped
v0.3Scenes & ECSSpaceInvadersshipped
v0.4Physics & Collision — Box2D 2D and Jolt 3D, then 3D inside the ECSAngryBirds, DungeonCrawler3Dshipped
v0.5Audio & Gameplay-Grade Physics — miniaudio, character controller, ray/shape castsEchoVaultshipped
v0.6Asset Pipeline & Hot-Reload — AssetManager, async loading, live reloadArenaShootershipped
v0.7Lighting & Shading — point/spot lights on a capped forward multi-light path, specularCandlewickshipped
v0.7.1Transform Hierarchy — parent-child transforms in 3D and 2D, world-space rendering, lights, audio and physicsDungeonCrawler3D, EchoVaultshipped
v0.8Animation & Character Fidelity — GPU-skinned meshes, clips, blending and layers, timeline events, joint socketsMarionetteshipped
v0.9Shadows, Post-processing & VFX — cascaded and local shadows, tone mapping, bloom, AO, GPU particlesCandlewick, Marionette upgradesshipped
v1.0Stability, Performance & Polish — docs, Linux validation, culling + instancingDungeon 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.

Getting Started

Prerequisites

  • Windows 10/11 with Visual Studio 2026, or Linux (x86-64; verified on Ubuntu 24.04) with GCC 13 or newer
  • Vulkan SDK 1.4 installed and the 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).

Examples

ProjectDescription
FlappyBirdComplete 2D game — sprites, input, collision, audio, score rendering
Breakout3D3D Breakout — perspective camera, mesh rendering, manual AABB collision
DungeonCrawlerTop-down 2D slice — tile collision, chasing enemies, melee combat, loot
SpaceInvadersScene/ECS showcase — EnTT entities and a multi-scene SceneManager
AngryBirds2D physics showcase — slingshot launching, destructible block towers, and pig targets on the Box2D-backed physics world
DungeonCrawler3D3D 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
EchoVaultv0.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
ArenaShooterv0.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)
Candlewickv0.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
Marionettev0.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

Project Structure

include/DingoEngine/    Public API
src/DingoEngine/        Engine implementation
vendor/                 Third-party dependencies (submodules)
examples/               Example projects

Dependencies

LibraryPurpose
NVRHIGraphics API abstraction (Vulkan / D3D12)
GLFWWindowing and input
GLMMath (vectors, matrices, quaternions)
spdlogLogging
Dear ImGuiDebug UI
stbImage loading
msdf-atlas-genFont SDF atlas generation
EnTTEntity-component system (scenes)
Box2D2D rigid-body physics simulation
Jolt Physics3D rigid-body physics simulation

Credits

*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.

Source 2 files
hooks/register.tsx 211 lines
1import { 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}
211
types/index.d.ts 30 lines
1export 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