TermUI.Renderer.FramerateLimiter (TermUI v0.2.0)

View Source

Caps rendering to a maximum FPS with dirty flag coalescing.

The FramerateLimiter schedules render cycles at regular intervals (default 60 FPS) and only renders when the buffer is dirty. Multiple buffer writes between frames coalesce into a single render, creating smooth animation while being efficient.

Features

  • Frame timing - Configurable FPS (30, 60, 120)
  • Drift compensation - Adjusts intervals based on actual elapsed time
  • Dirty coalescing - Multiple writes become single render
  • Immediate mode - Bypass frame timing for urgent updates
  • Performance metrics - Tracks FPS, render time, skip ratio

Usage

# Start with default 60 FPS
{:ok, pid} = FramerateLimiter.start_link(render_callback: fn -> :ok end)

# Start with custom FPS
{:ok, pid} = FramerateLimiter.start_link(fps: 120, render_callback: fn -> :ok end)

# Mark buffer as dirty (triggers render on next tick)
FramerateLimiter.mark_dirty()

# Force immediate render
FramerateLimiter.render_immediate()

# Get performance metrics
FramerateLimiter.stats()

Render Callback

The render callback is invoked on each frame tick when the buffer is dirty. It should perform the actual rendering work (diff, cursor optimization, etc.).

Summary

Functions

Returns a specification to start this module under a supervisor.

Clears the internal dirty flag (for standalone use without BufferManager).

Returns whether the internal dirty flag is set (for standalone use).

Returns the current target FPS.

Marks the internal dirty flag (for standalone use without BufferManager).

Pauses frame timing (stops render ticks).

Returns whether frame timing is paused.

Forces an immediate render, bypassing frame timing.

Resets performance statistics.

Resumes frame timing after pause.

Changes the target FPS.

Starts the FramerateLimiter.

Returns performance statistics.

Types

fps()

@type fps() :: 30 | 60 | 120

stats()

@type stats() :: %{
  rendered_frames: non_neg_integer(),
  skipped_frames: non_neg_integer(),
  total_frames: non_neg_integer(),
  actual_fps: float(),
  avg_render_time_us: float(),
  slow_frames: non_neg_integer()
}

t()

@type t() :: %TermUI.Renderer.FramerateLimiter{
  dirty_check: (-> boolean()),
  dirty_clear: (-> :ok),
  fps: fps(),
  frame_timestamps: [integer()],
  interval_ms: float(),
  last_tick: integer(),
  paused: boolean(),
  render_callback: (-> any()),
  render_times: [non_neg_integer()],
  rendered_frames: non_neg_integer(),
  skipped_frames: non_neg_integer(),
  slow_frames: non_neg_integer(),
  timer_ref: reference() | nil
}

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear_dirty(server \\ __MODULE__)

@spec clear_dirty(GenServer.server()) :: :ok

Clears the internal dirty flag (for standalone use without BufferManager).

When using BufferManager integration, this is called automatically via the dirty_clear callback after each render.

dirty?(server \\ __MODULE__)

@spec dirty?(GenServer.server()) :: boolean()

Returns whether the internal dirty flag is set (for standalone use).

When using BufferManager integration, call BufferManager.dirty?/1 instead.

get_fps(server \\ __MODULE__)

@spec get_fps(GenServer.server()) :: fps()

Returns the current target FPS.

mark_dirty(server \\ __MODULE__)

@spec mark_dirty(GenServer.server()) :: :ok

Marks the internal dirty flag (for standalone use without BufferManager).

When using BufferManager integration, call BufferManager.mark_dirty/1 instead.

pause(server \\ __MODULE__)

@spec pause(GenServer.server()) :: :ok

Pauses frame timing (stops render ticks).

paused?(server \\ __MODULE__)

@spec paused?(GenServer.server()) :: boolean()

Returns whether frame timing is paused.

render_immediate(server \\ __MODULE__)

@spec render_immediate(GenServer.server()) :: :ok

Forces an immediate render, bypassing frame timing.

Use for urgent updates that can't wait for the next tick.

reset_stats(server \\ __MODULE__)

@spec reset_stats(GenServer.server()) :: :ok

Resets performance statistics.

resume(server \\ __MODULE__)

@spec resume(GenServer.server()) :: :ok

Resumes frame timing after pause.

set_fps(server \\ __MODULE__, fps)

@spec set_fps(GenServer.server(), fps()) :: :ok

Changes the target FPS.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts the FramerateLimiter.

Options

  • :fps - Target FPS: 30, 60, or 120 (default: 60)
  • :render_callback - Function to call for rendering (required)
  • :dirty_check - Function returning true if render needed (optional)
  • :dirty_clear - Function to clear dirty flag after render (optional)
  • :name - GenServer name (default: __MODULE__)

If :dirty_check and :dirty_clear are not provided, an internal dirty flag is created. For integration with BufferManager, pass its dirty functions:

Examples

# Standalone with internal dirty flag
{:ok, pid} = FramerateLimiter.start_link(
  fps: 60,
  render_callback: fn -> render_frame() end
)

# With BufferManager integration
{:ok, pid} = FramerateLimiter.start_link(
  fps: 60,
  render_callback: fn -> render_frame() end,
  dirty_check: fn -> BufferManager.dirty?(manager) end,
  dirty_clear: fn -> BufferManager.clear_dirty(manager) end
)

stats(server \\ __MODULE__)

@spec stats(GenServer.server()) :: stats()

Returns performance statistics.

Returns a map with:

  • :rendered_frames - Number of frames rendered
  • :skipped_frames - Number of clean frames skipped
  • :total_frames - Total frame ticks
  • :actual_fps - Calculated FPS from recent frames
  • :avg_render_time_us - Average render time in microseconds
  • :slow_frames - Frames that exceeded target interval