---
title: "Measure CPU and GPU frame time in C#"
url: https://recursiveai.org/onegame/cookbook/measure-cpu-and-gpu-frame-time/
description: "Read CPU rendering time, GPU frame time and per-pass GPU timestamps in C# with OneGame, including unavailable timings and delayed frame indices."
version: "2.0.0"
---

Frame time measures rendering work in milliseconds. This example needs Vulkan (Linux or Windows) and GPU timestamp queries.

```shell
dotnet add package RecursiveAI.OneGame.Render3D
dotnet add package RecursiveAI.OneGpu.Backends
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneGpu;
using OneGame.Gpu.Graph;
using OneGame.Render3D;

using var device = GpuBackends.CreateDevice();
using var renderer = new Renderer3D(device);
using var target = new Offscreen3D(renderer, 640, 360);
var scene = new Scene3D();
for (int i = 0; i < 12; i++) target.Render(scene);
var stats = renderer.LastStats;
Console.WriteLine($"CPU: {stats.CpuFrameMs:F3} ms");
Console.WriteLine(stats.GpuFrameMs is double gpu
    ? $"GPU: {gpu:F3} ms (frame {stats.GpuFrameIndex})"
    : "GPU: unavailable");
```

Renders an empty 640 × 360 scene and prints CPU and GPU rendering time. One run on an NVIDIA RTX 4070 with Vulkan printed:

```text
CPU: 0.200 ms
GPU: 0.093 ms (frame 12)
```

Unzip [the example](https://recursiveai.org/onegame/cookbook/measure-cpu-and-gpu-frame-time/measure-cpu-and-gpu-frame-time.zip), then `dotnet run`.

## Full program

```csharp
using RecursiveAI.OneGpu;
using OneGame.Gpu.Graph;
using OneGame.Render3D;

using var device = GpuBackends.CreateDevice();
using var renderer = new Renderer3D(device);
using var target = new Offscreen3D(renderer, 640, 360);
var scene = new Scene3D();
for (int i = 0; i < 12; i++) target.Render(scene);
var stats = renderer.LastStats;
Console.WriteLine($"CPU: {stats.CpuFrameMs:F3} ms");
Console.WriteLine(stats.GpuFrameMs is double gpu
    ? $"GPU: {gpu:F3} ms (frame {stats.GpuFrameIndex})"
    : "GPU: unavailable");
Console.WriteLine($"Adapter: {device.Adapter.Name}");
Console.WriteLine($"Submitted frame: {target.Graph.Frame}");
var passes = new List<PassTime>();
int frame = renderer.ReadGpuPassTimes(passes);
Console.WriteLine($"Pass frame: {frame}");
foreach (var pass in passes)
    Console.WriteLine($"{pass.Name}: {pass.Microseconds:F3} us");
```

The snippet at the top and the full program were run separately, so their CPU times (0.200 and 0.100 ms) differ. The download also prints the adapter and the GPU pass breakdown. Its scene has no models or input files. These are individual measurements, not expected values for your game.

```text
CPU: 0.100 ms
GPU: 0.094 ms (frame 12)
Adapter: NVIDIA GeForce RTX 4070
Submitted frame: 12
Pass frame: 12
forward: 13.312 us
bloom_down_0: 4.096 us
bloom_down_1: 2.048 us
bloom_down_2: 1.024 us
bloom_down_3: 1.024 us
bloom_down_4: 1.024 us
bloom_up_4: 1.024 us
bloom_up_3: 1.024 us
bloom_up_2: 2.048 us
bloom_up_1: 3.072 us
tonemap: 4.096 us
```

Without finished GPU timestamps the program prints `GPU: unavailable`; a missing graphics backend can throw `GpuUnsupportedException` when creating the device.

## Related recipes

- [Render planar reflections on water](https://recursiveai.org/onegame/cookbook/planar-reflection-water/)
- [Capture game screenshots in tests](https://recursiveai.org/onegame/cookbook/screenshot-tests/)

## The short answer

Read `Renderer3D.LastStats` after rendering and executing the graph. `CpuFrameMs` gives CPU rendering time in milliseconds. `GpuFrameMs` gives a nullable GPU duration; use `GpuFrameIndex` to identify its frame. For a pass breakdown, call `ReadGpuPassTimes` with a reusable list.

The CPU measurement belongs to the rendering work just submitted, while the GPU result describes the newest finished graph frame. The [pinned frame-time documentation](https://github.com/recursive-ai-oss/onegame/blob/60d5573da204de68cafd5896dc8d41b16c72ff9d/docs/interfaces/render3d.md#frame-time) says GPU results are usually one or two frames behind on a real GPU. Do not assume that the two numbers printed together describe the same frame.

## How do you measure frame time rather than FPS?

Use the renderer’s measured durations, not the interval between calls to your update loop. This program renders offscreen: it does not need a window, a swapchain or a displayed FPS counter. It submits twelve frames, then reads the stats. The scene is deliberately empty, so the output demonstrates the measurement API rather than a particular game workload.

In a windowed game, read the stats each frame after executing the graph that contains `renderer.Render(...)`. Here, `graph` and `renderer` are the game’s existing render graph and 3D renderer:

```csharp
// Inside the windowed render loop, after renderer.Render(...):
graph.Execute();
var stats = renderer.LastStats;
```

`CpuFrameMs` measures the `Renderer3D.Render` call plus the following `RenderGraph.Execute`. That includes scene preparation, uploads, pass declaration, command recording and submission. Before graph execution it contains only the graph-building part. It is not your whole game-loop duration: simulation, input processing and application work outside those calls are not included in that definition.

`GpuFrameMs` spans the first timed pass’s start through the last timed pass’s end. Gaps between passes count. If 2D or UI work shares the graph, it is included in that GPU interval too. These scopes come from the documentation linked above; neither field is a presentation-latency measurement.

## How do I read CPU versus GPU frame time?

Read the units and frame identity before interpreting the values. The frame-level fields use milliseconds. The per-pass records use microseconds. In the output above, `Submitted frame` identifies the graph submission and `Pass frame` identifies the finished GPU work returned by the timing read.

The read does not wait or submit more GPU work. A frame that is still running is skipped for an older finished frame. The timing ring is four frames deep, and the documented lag is at most three frames. An older valid result is not an error, and a matching index in this particular offscreen run is not a promise that indices always match.

## How can I see GPU frame time when no result is available yet?

Continue rendering and check the nullable field again on later frames. Do not turn `null` into zero: zero would imply a measured duration, while `null` says that there is no available measurement.

There are three documented reasons: the device lacks timestamp-query support, graph timing is disabled, or no timed frame has finished. Check `device.Capabilities.Timestamps` when selecting a profiling path. At this release, Metal and WebGPU do not provide these timestamps. Disabling timing with `RenderGraphOptions.Timing = false` also leaves GPU frame time unavailable. In those cases the GPU index is zero and `ReadGpuPassTimes` returns zero; CPU rendering time remains available after rendering.

The pass breakdown uses existing graph timestamps. Its returned integer identifies the frame that supplied the list. An empty or unavailable breakdown is not evidence that your scene performed no rendering.
