Skip to content

LOD budget system: auto-adjust thresholds for target frame time #82

@brendancol

Description

@brendancol

Context

Follow-up to #74. Roadmap Phase 2 item (see #57). Terrain LOD (#78), instanced geometry LOD (#80), and hybrid cluster/standard GAS (#81) each use fixed distance thresholds. These work well for a given scene scale but don't adapt to scene complexity — a 10M-triangle city scene needs more aggressive LOD than a sparse rural DEM. A budget system closes the loop: measure actual frame time, adjust LOD thresholds to hit a target.

Goal

Automatic LOD threshold adjustment that maintains a target frame time (e.g. 33ms for 30fps) by pushing LOD transitions closer or farther based on measured render performance.

Current State

  • TerrainLODManager uses compute_lod_distances() with a fixed lod_distance_factor — all thresholds scale linearly from tile diagonal
  • Instanced geometry LOD (Instanced geometry LOD: mesh LOD chains and billboard imposters #80) will use compute_lod_level() with fixed thresholds
  • Frame timing is available in the viewer tick loop (_update_frame() returns, GLFW provides glfwGetTime())
  • No feedback loop exists between render cost and LOD parameters

Design

Feedback Loop

measure frame_time
    → compare to target_frame_time
    → compute error = frame_time - target
    → adjust lod_distance_factor (controls all LOD thresholds)
    → LOD managers recompute distances on next update

Controller

A simple exponential moving average + proportional controller:

  • ema_frame_time = α * frame_time + (1-α) * ema_frame_time (α ≈ 0.1, smooth out spikes)
  • If ema_frame_time > target * 1.1: decrease lod_distance_factor by step (push LOD transitions closer → fewer triangles)
  • If ema_frame_time < target * 0.9: increase lod_distance_factor by step (push transitions farther → more detail)
  • Dead band (±10%) prevents oscillation when frame time is close to target
  • Step size proportional to error magnitude, clamped to avoid large jumps

Bounds

  • min_lod_distance_factor: floor to prevent quality collapse (e.g. 1.0 — LOD transitions no closer than 1× tile diagonal)
  • max_lod_distance_factor: ceiling to prevent wasted work when GPU is fast (e.g. 10.0)
  • Per-frame adjustment clamped to ±5% of current factor — smooth, no pops

Budget Metrics (Beyond Frame Time)

Optional secondary metrics for more precise control:

Metric Source Use
Total triangle count Sum of active GAS triangle counts Hard cap: if > budget, force LOD up regardless of frame time
Total instance count IAS instance count Similar hard cap
GPU memory pressure cuMemGetInfo free/total Force aggressive LOD when memory is tight

Frame time is the primary metric. Triangle/instance/memory caps are safety valves, not the main control loop.

Integration Points

LODBudgetController API

class LODBudgetController:
    def __init__(self, target_fps=30, min_factor=1.0, max_factor=10.0,
                 alpha=0.1, dead_band=0.1, step_scale=0.02):
        ...

    def update(self, frame_time_ms):
        """Called once per frame. Returns adjusted lod_distance_factor."""
        ...

    @property
    def current_factor(self):
        ...

    @property
    def ema_frame_time(self):
        ...

Viewer Integration

  • Toggle: Shift+B or similar keybinding — enables/disables auto-budget
  • HUD: show target FPS, current EMA frame time, current lod_distance_factor, and direction indicator (↑ increasing detail / ↓ decreasing detail / = stable)
  • When disabled, lod_distance_factor stays at its last value (no snap back to default)
  • Manual LOD distance override (if added later) disables auto-budget

Implementation Plan

  1. LODBudgetController class — in rtxpy/lod.py, pure logic, no viewer dependencies, fully unit-testable
  2. Frame time measurement — capture render time per frame in the tick loop (wall clock, not including present/vsync)
  3. Wire controller to TerrainLODManager — adjust lod_distance_factor, call compute_lod_distances() to update thresholds
  4. Wire to instanced LOD (Instanced geometry LOD: mesh LOD chains and billboard imposters #80) — same factor or per-layer multiplier
  5. Wire to hybrid GAS (Hybrid cluster/standard GAS selection by distance #81) — cluster distance threshold
  6. HUD display — budget status line in help overlay
  7. Toggle keybinding — Shift+B, add to SHIFT_BINDINGS table
  8. Triangle count safety cap — optional hard limit, force LOD up if exceeded

Scope Boundaries

  • Proportional controller only — no PID (derivative/integral terms add complexity without clear benefit for this use case)
  • Frame time is the primary signal; triangle count cap is stretch goal
  • No per-geometry priority system (all geometry LOD adjusts uniformly)
  • No network/disk streaming budget — this is purely GPU render budget

Key Files

  • rtxpy/lod.pycompute_lod_distances(), new LODBudgetController
  • rtxpy/viewer/terrain_lod.pyTerrainLODManager (consumes adjusted factor)
  • rtxpy/engine.py — tick loop, frame timing, budget controller instance
  • rtxpy/viewer/keybindings.pySHIFT_BINDINGS table

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions