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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions