Class: RGame::Engine::DebugOverlay

Inherits:
Object
  • Object
show all
Defined in:
lib/rgame/engine/debug_overlay.rb

Overview

A hard-wired development overlay, toggled by F1 (see RGame::Game), reporting runtime health in the bottom-right corner: frame rate (FPS), the process' cumulative allocated-object count (OBJ), and the objects allocated since the last frame (Δ/f).

Δ/f is the one to watch, and it is a standing guard rather than a diagnostic for one past bug. A clean per-frame path holds it near zero; a steady nonzero number is a garbage collection being scheduled. The cost is invisible by every other measure — nothing looks wrong, nothing is slower, until a pause lands mid-frame — so without a number on screen it is not noticed at all.

Every layer can break it and each has its own way of doing so. Core can allocate in a binding, the engine layer in a component's draw, and game code in a scene that builds a string or an array per frame. RuboCop's Game/NoInterpolationInHotPath and Game/NoNeedlessAllocation catch the shapes they can see in this repo; they cannot see a game built on top, and they cannot see an allocation that happens inside a method they think is cheap. This can.

Pure: it reads only GC.stat and draws against the renderer interface, so it stays headless-testable. The crux is that it must not allocate while drawing — the values change every frame, so the usual "cache the string, rebuild on change" trick would allocate a String per frame, and the meter would be measuring itself. Instead numbers are drawn digit-by-digit from a fixed set of pre-built single-character strings, which the font caches per glyph.

Constant Summary collapse

DIGITS =
%w[0 1 2 3 4 5 6 7 8 9].freeze
FPS_LABEL =
'FPS'
OBJ_LABEL =
'OBJ'
DELTA_LABEL =
'Δ/f'
COLOR =

frozen so the renderer caches the resolved colour

[80, 255, 120].freeze
PAD =

margin from the screen edge

8
GAP =

space between a label and its number

8

Instance Method Summary collapse

Constructor Details

#initializeDebugOverlay

Returns a new instance of DebugOverlay.



42
43
44
45
46
47
# File 'lib/rgame/engine/debug_overlay.rb', line 42

def initialize
  @visible = false
  @prev_allocated = 0
  @digit_widths  = Array.new(10) # measured once, then reused
  @label_widths  = {}            # measured once per label, then reused
end

Instance Method Details

#draw(renderer, view, fps) ⇒ Object

Laid out against the view it is drawn into rather than against the window, so it stays in the corner of whatever region it is given. It takes fps rather than reading it, for the same reason nothing here reads a clock: the number is measured by the shell that owns the loop and handed down. See CLAUDE.md, "draw renders state". Its own :debug band, which is the last one, so this lands over every other thing in the frame however the scene is arranged. Not a node, so it opens its own layer rather than being given one by the traversal.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/rgame/engine/debug_overlay.rb', line 65

def draw(renderer, view, fps)
  return unless @visible

  allocated = GC.stat(:total_allocated_objects)
  delta = allocated - @prev_allocated
  @prev_allocated = allocated

  line_h  = renderer.text_height
  # The view's size, not its position: inside a region that has been
  # translated to its corner, adding `view.x` would offset a second time.
  right_x = view.width - PAD
  top_y   = view.height - PAD - (line_h * 3)

  renderer.layered(:debug) do
    draw_line(renderer, FPS_LABEL, fps, right_x, top_y)
    draw_line(renderer, OBJ_LABEL, allocated, right_x, top_y + line_h)
    draw_line(renderer, DELTA_LABEL, delta, right_x, top_y + (line_h * 2))
  end
end

#toggleObject



51
52
53
54
55
# File 'lib/rgame/engine/debug_overlay.rb', line 51

def toggle
  @visible = !@visible
  # Baseline the counter on reveal so the first Δ/f isn't the whole run's history.
  @prev_allocated = GC.stat(:total_allocated_objects) if @visible
end

#visible?Boolean

Returns:

  • (Boolean)


49
# File 'lib/rgame/engine/debug_overlay.rb', line 49

def visible? = @visible