diff --git a/lib/cyberarm_engine/stats.rb b/lib/cyberarm_engine/stats.rb index 3dbb8d7..dc7a89e 100644 --- a/lib/cyberarm_engine/stats.rb +++ b/lib/cyberarm_engine/stats.rb @@ -42,15 +42,17 @@ module CyberarmEngine class Frame Timing = Struct.new(:start_time, :end_time, :duration) - attr_reader :frame_timing, :counters, :timings + attr_reader :frame_timing, :counters, :timings, :multitimings def initialize @frame_timing = Timing.new(start_time: Gosu.milliseconds, end_time: -1, duration: -1) + @attempted_multitiming = false @counters = { gui_recalculations: 0 } @timings = {} + @multitimings = {} end def increment(key, number = 1) @@ -60,7 +62,13 @@ module CyberarmEngine def start_timing(key) raise "key must be a symbol!" unless key.is_a?(Symbol) - warn "Only one timing per key per frame. (Timing for #{key.inspect} already exists!)" if @timings[key] + if @timings[key] + warn "Only one timing per key per frame. (Timing for #{key.inspect} already exists!)" + @attempted_multitiming = true + @multitimings[key] = true + + return + end @timings[key] = Timing.new(start_time: Gosu.milliseconds, end_time: -1, duration: -1) end @@ -82,11 +90,101 @@ module CyberarmEngine @frame_timing.freeze @counters.freeze @timings.freeze + @multitimings.freeze end def complete? @frame_timing.duration != -1 end + + def attempted_multitiming? + @attempted_multitiming + end + end + + class StatsPlotter + attr_reader :position + + def initialize(x, y, z = Float::INFINITY, width = 128, height = 128) + @position = Vector.new(x, y, z) + @width = width + @height = height + + @padding = 2 + @text_size = 16 + + @max_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding, z: z, size: @text_size, border: true) + @avg_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding + @height / 2 - @text_size / 2, z: z, size: @text_size, border: true) + @min_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @height - (@text_size + @padding / 2), z: z, size: @text_size, border: true) + + @timings_label = CyberarmEngine::Text.new("", x: x + @padding + @width + @padding, y: y + @padding, z: z, size: @text_size, border: true) + + @frame_stats = [] + @graphs = { + frame_timings: [] + } + end + + def calculate_graphs + calculate_frame_timings_graph + end + + def calculate_frame_timings_graph + @graphs[:frame_timings].clear + + samples = @width - @padding + nodes = Array.new(samples.ceil) { [] } + + slice = 0 + @frame_stats.each_slice((CyberarmEngine::Stats.max_frame_history / samples.to_f).ceil) do |bucket| + bucket.each do |frame| + nodes[slice] << frame.frame_timing.duration + end + + slice += 1 + end + + nodes.each_with_index do |cluster, i| + break if cluster.empty? + + @graphs[:frame_timings] << CyberarmEngine::Vector.new(@position.x + @padding + 1 * i, (@position.y + @height - @padding) - cluster.max) + end + end + + def draw + @frame_stats = CyberarmEngine::Stats.frames.select(&:complete?) + return if @frame_stats.empty? + + calculate_graphs + + @max_timing_label.text = "Max: #{@frame_stats.map { |f| f.frame_timing.duration }.max.to_s.rjust(3, " ")}ms" + @avg_timing_label.text = "Avg: #{(@frame_stats.map { |f| f.frame_timing.duration }.sum / @frame_stats.size).to_s.rjust(3, " ")}ms" + @min_timing_label.text = "Min: #{@frame_stats.map { |f| f.frame_timing.duration }.min.to_s.rjust(3, " ")}ms" + + Gosu.draw_rect(@position.x, @position.y, @width, @height, 0xaa_222222, @position.z) + Gosu.draw_rect(@position.x + @padding, @position.y + @padding, @width - @padding * 2, @height - @padding * 2, 0xaa_222222, @position.z) + + draw_graphs + + @max_timing_label.draw + @avg_timing_label.draw + @min_timing_label.draw + + # TODO: Make this optional + draw_timings + end + + def draw_graphs + Gosu.draw_path(@graphs[:frame_timings], Gosu::Color::WHITE, Float::INFINITY) + end + + def draw_timings + frame = @frame_stats.last + + @timings_label.text = "#{frame.attempted_multitiming? ? "Attempted Multitiming!\nTimings may be inaccurate for:\n#{frame.multitimings.map { |m, _| m}.join("\n") }\n\n" : ''}#{frame.timings.map { |t, v| "#{t}: #{v.duration}ms" }.join("\n")}" + Gosu.draw_rect(@timings_label.x - @padding, @timings_label.y - @padding, @timings_label.width + @padding * 2, @timings_label.height + @padding * 2, 0xdd_222222, @position.z) + @timings_label.draw + end end end end diff --git a/lib/cyberarm_engine/window.rb b/lib/cyberarm_engine/window.rb index ab3cadd..5cd5c46 100644 --- a/lib/cyberarm_engine/window.rb +++ b/lib/cyberarm_engine/window.rb @@ -6,7 +6,7 @@ module CyberarmEngine SAMPLES = {} SONGS = {} - attr_accessor :show_cursor + attr_accessor :show_cursor, :show_stats_plotter attr_writer :exit_on_opengl_error attr_reader :last_frame_time, :delta_time, :states @@ -31,6 +31,7 @@ module CyberarmEngine def initialize(width: 800, height: 600, fullscreen: false, update_interval: 1000.0 / 60, resizable: false, borderless: false) @show_cursor = false @has_focus = false + @show_stats_plotter = false super(width, height, fullscreen: fullscreen, update_interval: update_interval, resizable: resizable, borderless: borderless) Window.instance = self @@ -43,6 +44,7 @@ module CyberarmEngine @states = [] @exit_on_opengl_error = false preload_default_shaders if respond_to?(:preload_default_shaders) + @stats_plotter = Stats::StatsPlotter.new(2, 28) # FIXME: Make positioning easy setup end @@ -54,6 +56,9 @@ module CyberarmEngine Stats.frame.start_timing(:draw) current_state&.draw + Stats.frame.start_timing(:engine_stats_renderer) + @stats_plotter&.draw if @show_stats_plotter + Stats.frame.end_timing(:engine_stats_renderer) Stats.frame.end_timing(:draw) Stats.frame.start_timing(:interframe_sleep)