Skip to content

Interpolation analysis#24

Open
gimploo wants to merge 1 commit into
devfrom
fix/windows-timer-interpolation
Open

Interpolation analysis#24
gimploo wants to merge 1 commit into
devfrom
fix/windows-timer-interpolation

Conversation

@gimploo

@gimploo gimploo commented Jun 30, 2026

Copy link
Copy Markdown
Owner

No Render Interpolation (analysis — not implemented)

Even with 1ms timer precision, on a display with refresh rate ≠ 60Hz, the render loop produces duplicate frames. The alpha value at application.h:175-179 is already calculated but unused:

//TODO: this is used to interpolate bw frames...
//const f32 alpha = timer.accumulator / FIXED_DT;

The Alpha Formula (Gaffer on Games "Fix Your Timestep"):

alpha = accumulator / FIXED_DT         // range [0.0, 1.0]
render_state = lerp(previous_state, current_state, alpha)
  • alpha ≈ 0 (just after update): renders previous state (1-frame lag, imperceptible at 60Hz)
  • alpha ≈ 0.5 (midway): smooth blend between states
  • alpha ≈ 1 (before next update): renders current state

Why the render queue architecture complicates this:

Currently, render commands are generated inside ecs_update(), which runs during the fixed update loop (while(accumulator >= FIXED_DT)). But alpha exists only during the render phase (between update ticks). This means:

  1. During update, alpha is effectively 0 — render commands contain exact current-frame transforms
  2. We cannot blend previous/current states because render commands are baked into the queue during update, not render

Proposed Architecture Change:

Move render command generation from the update loop to the render phase:

GetBack_update (per tick):       GetBack_render (per frame):
  ecs_update()                     renderqueue_flush()
    → input                         ecs_render(alpha)
    → transform                       → render_model (with lerp)
    → collider                        → render_mesh (with lerp)
    → camera                        renderqueue_dispatch()
                                   scene_render()

Implementation Plan (6 files):

A) ecs/component/types.h — Store previous transform state:

struct ecs_component_transform_t {
    vec3f_t position;
    versors orientation;
    vec3f_t scale;
    vec3f_t previous_position;      // NEW: for interpolation
    versors previous_orientation;   // NEW: for interpolation
    // ...
};

B) ecs/systems/transform.h — Save current→previous before updating:

// In ecs_system_transfrom__internal_source_manual, before writing:
transform->previous_position    = transform->position;
transform->previous_orientation = transform->orientation;
transform->position             = input->internal.state.current_position;
transform->orientation          = normalize(input->internal.state.current_orientation);

C) ecs/common.h — Pass alpha through system context:

struct ecs_system_ctx_t {
    glcamera_t *active_camera;
    f32 dt;
    f32 alpha;  // NEW: interpolation factor [0.0, 1.0]
};

D) ecs/systems/model.h and mesh.h — Lerp transforms for render:

// When ctx.alpha > 0:
const vec3f_t pos = glms_vec3_lerp(transform->previous_position,
                                    transform->position, ctx.alpha);
const versors ori = glms_quat_slerp(transform->previous_orientation,
                                     transform->orientation, ctx.alpha);
// Build transform matrix from interpolated pos/ori

E) src/scenes/collision-scene.h — Separate render systems from update:

  • Remove ecs_system_render_model and ecs_system_render_mesh from main system list
  • Register them in a separate render-only list (requires adding render system array to ecs_t)

F) src/main.c — Restructure update/render calls:

void GetBack_update(application_t *const app) {
    // Only game logic — no render queue population
    bgtask_manager_run_all_tasks(global_bgtask_manager);
    poggen_update(content->game.engine);
    ecs_update(global_ecs);         // input, transform, collider, camera only
    workbench_update(FIXED_DT);
    commandqueue_flush(&global_engine->systems.commandqueue);
}

void GetBack_render(application_t *const app, const f32 dt, const f32 alpha) {
    renderqueue_flush(&global_engine->systems.renderqueue);
    ecs_render(global_ecs, alpha);  // render_model + render_mesh with lerp
    workbench_render();
    poggen_render(c->game.engine, dt);
}

And in application.h:179, uncomment and pass alpha:

const f32 alpha = timer.accumulator / FIXED_DT;
app->render(app, timer.raw_dt, alpha);

Additional: Camera Interpolation

The camera system (ecs/systems/camera.h) also needs previous state for smooth view matrix:

  • Store previous camera position and euler_angle
  • In render, compute interpolated view matrix: glcamera_set(lerped_position, lerped_angles)

Additional: Animation Bone Interpolation

Bone transforms in model->transforms are advanced at fixed 60Hz by the animation system. Without interpolation, skeletal animation would stutter. This requires:

  • Keeping previous bone transform snapshots per-model
  • Interpolating bone matrices during render using alpha
  • Larger scope, can be deferred to a follow-up PR

Summary

Issue Status Description
Timer precision ✅ Fixed here timeBeginPeriod(1) at startup
Render interpolation 📋 Analyzed here 6-file refactor, implement in follow-up PR
Camera interpolation 📋 Planned Included in interpolation PR
Bone animation interp 📋 Deferred Separate follow-up

@gimploo gimploo changed the title fix: Windows frame jitter — timer precision + interpolation analysis Interpolation analysis Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant