> "In C, the assignment has no influence on how the expression is evaluated."
Issue 0x1 was supposed to be straightforward: calculate delta time, expose it to the game loop. The math seemed simple - `SDL_GetPerformanceCounter()` gives hardware ticks, divide by frequency, done.
I overcomplicated it in three ways.
## Breakpoint #1: Global State
My first implementation used file-level globals for `last_tick`, `current_tick`, and `delta_time`. This worked, but it undid the entire point of 0x0, which was to extract static globals out of `main.c` and into an `Engine` struct to establish ownership.
**Fix:** Embed a `Timer` struct inside `Engine`. The timer belongs to the engine. It dies when the engine dies. No globals, no ambiguity.
## Breakpoint #2: Everything Is a Pointer
When I rewrote the Timer struct, my Rust brain kicked in. I started making everything a pointer:
```
// My instinct from Rust
typedef struct Engine {
Timer *timer; // pointer — because Rust taught me to borrow
} Engine;
```
In Rust, `&` and `&mut` are borrows - temporary views into data. In C, a pointer is just a memory address. Nobody tracks it. Making `timer` a pointer meant `malloc` and `free`, extra allocation, extra failure points, for a 24-byte struct that's owned entirely by Engine.
**Fix:** Embed by value. `Timer timer;` lives inside `Engine`. No allocation. No lifetime tracking.
## Breakpoint #3: Integer Division Truncation
The real gotcha arrived when the delta time printed as `0.000000` even though the ticks were advancing correctly.
**My initial diagnosis was wrong.**
I thought: "Maybe `frequency` didn't initialize, so I divided by zero? Or maybe the result got truncated to a float?"
Neither was true. The actual problem was invisible to me until I logged the intermediate values.
**The broken math:**
```
Uint64 last_tick = 0;
Uint64 current_tick = SDL_GetPerformanceCounter(); // 13,912,992,878,646 Uint64 frequency = SDL_GetPerformanceFrequency(); // 1,000,000,000
// The calculation
delta_time = (current_tick - last_tick) / frequency;
// 13,912,992 / 1,000,000,000
```
Here's where C and Rust diverge.
**In Rust:**
```
let delta: f64 = ticks / frequency; // If either side is f64, Rust promotes automatically
let delta: f64 = (ticks as f64) / (frequency as f64); // Explicit cast
```
Rust either complains or promotes based on the destination type. The compiler doesn't let you silently lose precision.
**In C:**
C evaluates expressions **before** assignment. The destination type has zero influence on the calculation.
```
delta_time = (current_tick - last_tick) / frequency;
// ALL Uint64, integer division
```
1. First frame `last_tick` was set to `0` in `engine_init`
2. First tick difference was ~316,000
3. `316,000 / 1,000,000,000` equals `0` (integer division)
4. `0` assigned to `double` becomes `0.0`
**The fix:**
Cast **one operand** before the division, not after:
```// WRONG — cast after calculation
delta_time = (double)((current_tick - last_tick) / frequency); // Still integer division
// CORRECT — cast before calculation
delta_time = (double)(current_tick - last_tick) / (double)frequency;
```
Now C promotes both sides to `double` before the division happens. The result is `0.000316` seconds, correct for a 317 FPS loop.
**The lesson:**
In C, the type of the destination variable does not determine how the expression is calculated. You control the arithmetic by casting operands, not the result.
> `a / b` evaluated in integer arithmetic, then assigned to double ≠ `(double)a / (double)b` evaluated in floating-point arithmetic
This is a silent killer. No warning. No crash. Just wrong numbers that look plausible.
## What Shipped
- `Timer` struct embedded in `Engine` by value
- `SDL_GetPerformanceCounter()` for nanosecond precision (vs wall clock)
- Frequency cached in `engine_init` to avoid per-frame function calls
- `engine_update(Engine*)` computes delta time each frame
- Delta time clamped to 0.05s max (prevents physics jumps on window drag)
- **Double-casting operands** to force floating-point division
- No globals, no `extern`, no unnecessary pointers
Issue 0x1 done.
Next up: **[[0x2005_Issue 0x2, 0x3 - Input]]** — Keyboard state, `keys_down` and `keys_just_pressed`.
---
_[
[email protected]](mailto:
[email protected]) | Previous: [[0x2003_Issue 0x0 - Extraction]] → Next: [[0x2005_Issue 0x2, 0x3 - Input]]_
>_Transparency note: I use Lumo as a technical reviewer for these posts — checking C semantics, verifying claims, and catching formatting issues. The ideas, mistakes, and fixes are my own._