rbgl 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.codespellignore +4 -0
  3. data/.rubocop.yml +3 -0
  4. data/.yamllint +16 -0
  5. data/CHANGELOG.md +11 -1
  6. data/README.md +130 -74
  7. data/Rakefile +7 -0
  8. data/bench/renderer_bench.rb +283 -0
  9. data/docs/.nojekyll +1 -0
  10. data/docs/index.html +38 -0
  11. data/examples/array_test.rb +1 -1
  12. data/examples/color_test.rb +1 -1
  13. data/examples/cube_spinning.rb +1 -1
  14. data/examples/gradient.rb +1 -1
  15. data/examples/multi_return_test.rb +1 -1
  16. data/examples/plasma.rb +1 -1
  17. data/examples/sphere_raymarch.rb +1 -1
  18. data/examples/triangle_window.rb +1 -1
  19. data/exe/rbgl +42 -0
  20. data/lib/rbgl/cli/doctor.rb +155 -0
  21. data/lib/rbgl/engine/buffer.rb +55 -9
  22. data/lib/rbgl/engine/clip_space_clipper.rb +23 -3
  23. data/lib/rbgl/engine/context.rb +23 -6
  24. data/lib/rbgl/engine/framebuffer.rb +146 -41
  25. data/lib/rbgl/engine/immutable_color.rb +32 -0
  26. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +100 -10
  27. data/lib/rbgl/engine/rasterizer/line_renderer.rb +30 -14
  28. data/lib/rbgl/engine/rasterizer/point_renderer.rb +3 -3
  29. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +72 -23
  30. data/lib/rbgl/engine/rasterizer.rb +3 -20
  31. data/lib/rbgl/engine/shader/base_shader.rb +3 -7
  32. data/lib/rbgl/engine/shader/builtins.rb +13 -10
  33. data/lib/rbgl/engine/shader/dynamic_data.rb +18 -10
  34. data/lib/rbgl/engine/texture.rb +74 -50
  35. data/lib/rbgl/engine.rb +3 -1
  36. data/lib/rbgl/gui/backend.rb +12 -7
  37. data/lib/rbgl/gui/backend_factory.rb +7 -3
  38. data/lib/rbgl/gui/cocoa/backend.rb +50 -17
  39. data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
  40. data/lib/rbgl/gui/event.rb +4 -2
  41. data/lib/rbgl/gui/file_backend/bmp_writer.rb +2 -2
  42. data/lib/rbgl/gui/file_backend/frame_writer.rb +7 -0
  43. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  44. data/lib/rbgl/gui/file_backend/ppm_writer.rb +1 -0
  45. data/lib/rbgl/gui/file_backend.rb +24 -6
  46. data/lib/rbgl/gui/wayland/backend.rb +127 -55
  47. data/lib/rbgl/gui/wayland/codec.rb +7 -11
  48. data/lib/rbgl/gui/wayland/connection.rb +137 -20
  49. data/lib/rbgl/gui/wayland/event_dispatcher.rb +103 -12
  50. data/lib/rbgl/gui/wayland/global_binder.rb +6 -4
  51. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +14 -2
  53. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +23 -12
  55. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +15 -0
  56. data/lib/rbgl/gui/wayland/protocol_objects.rb +1 -0
  57. data/lib/rbgl/gui/window/render_loop.rb +71 -18
  58. data/lib/rbgl/gui/window.rb +39 -8
  59. data/lib/rbgl/gui/x11/atom_cache.rb +4 -1
  60. data/lib/rbgl/gui/x11/backend.rb +66 -28
  61. data/lib/rbgl/gui/x11/connection.rb +216 -86
  62. data/lib/rbgl/gui/x11/event_parser.rb +13 -2
  63. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  64. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  65. data/lib/rbgl/gui/x11/request_encoder.rb +29 -28
  66. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  67. data/lib/rbgl/gui/x11/transport.rb +52 -2
  68. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  69. data/lib/rbgl/gui.rb +2 -5
  70. data/lib/rbgl/version.rb +1 -1
  71. metadata +38 -24
  72. data/examples/chemical_heartbeat.rb +0 -166
  73. data/examples/dark_transit.rb +0 -166
  74. data/examples/fractured_orb.rb +0 -428
  75. data/examples/fractured_orb_rb.rb +0 -598
  76. data/examples/hexagonal_flow.rb +0 -333
  77. data/examples/teapot.rb +0 -362
  78. data/examples/teapot_mcu.rb +0 -344
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "input_mapper"
4
+
3
5
  module RBGL
4
6
  module GUI
5
7
  module Wayland
@@ -13,16 +15,24 @@ module RBGL
13
15
  object = @connection.object_for(object_id)
14
16
 
15
17
  case object
18
+ when Display
19
+ handle_display_event(opcode, payload)
16
20
  when Registry
17
21
  handle_registry_event(opcode, payload)
18
22
  when Callback
19
23
  object.handle_done if opcode == 0
20
24
  when WlBuffer
21
25
  object.handle_release if opcode == 0
26
+ when Seat
27
+ object.handle_capabilities(payload.unpack1("V")) if opcode == 0
28
+ when Keyboard
29
+ handle_keyboard_event(object, opcode, payload)
30
+ when Pointer
31
+ handle_pointer_event(object, opcode, payload)
22
32
  when XdgWmBase
23
33
  handle_xdg_wm_base_event(object, opcode, payload)
24
34
  when XdgSurface
25
- handle_xdg_surface_event(object, opcode, payload)
35
+ object.handle_configure(payload.unpack1("V")) if opcode == 0
26
36
  when XdgToplevel
27
37
  handle_xdg_toplevel_event(object_id, opcode, payload)
28
38
  end
@@ -30,14 +40,29 @@ module RBGL
30
40
 
31
41
  private
32
42
 
33
- def handle_registry_event(opcode, payload)
34
- return unless opcode == 0
43
+ def handle_display_event(opcode, payload)
44
+ case opcode
45
+ when 0
46
+ failed_object, code, length = payload.byteslice(0, 12).unpack("VVV")
47
+ message = payload.byteslice(12, length - 1)
48
+ raise GUI::BackendUnavailable,
49
+ "Wayland protocol error #{code} on object #{failed_object}: #{message}"
50
+ when 1
51
+ @connection.unregister_object(payload.unpack1("V"))
52
+ end
53
+ end
35
54
 
36
- name = payload[0, 4].unpack1("V")
37
- interface_length = payload[4, 4].unpack1("V")
38
- interface = payload[8, interface_length - 1]
39
- version = payload[8 + @codec.pad_length(interface_length), 4].unpack1("V")
40
- @connection.store_global(interface, name: name, version: version)
55
+ def handle_registry_event(opcode, payload)
56
+ case opcode
57
+ when 0
58
+ name = payload[0, 4].unpack1("V")
59
+ interface_length = payload[4, 4].unpack1("V")
60
+ interface = payload[8, interface_length - 1]
61
+ version = payload[8 + @codec.pad_length(interface_length), 4].unpack1("V")
62
+ @connection.store_global(interface, name: name, version: version)
63
+ when 1
64
+ @connection.remove_global(payload.unpack1("V"))
65
+ end
41
66
  end
42
67
 
43
68
  def handle_xdg_wm_base_event(object, opcode, payload)
@@ -47,11 +72,77 @@ module RBGL
47
72
  @connection.flush
48
73
  end
49
74
 
50
- def handle_xdg_surface_event(object, opcode, payload)
51
- return unless opcode == 0
75
+ def handle_keyboard_event(keyboard, opcode, payload)
76
+ case opcode
77
+ when 0
78
+ format, size = payload.unpack("V2")
79
+ keyboard.handle_keymap(format, size, @connection.consume_received_fd)
80
+ when 1
81
+ keyboard.focus(payload.byteslice(4, 4).unpack1("V"))
82
+ when 2
83
+ keyboard.blur
84
+ when 3
85
+ _serial, _time, keycode, state = payload.unpack("V4")
86
+ event = {
87
+ type: state == 1 ? :key_press : :key_release,
88
+ surface_id: keyboard.focused_surface_id,
89
+ key: InputMapper.key(keycode),
90
+ keycode: keycode
91
+ }
92
+ event[:modifiers] = keyboard.modifiers.dup unless keyboard.modifiers.empty?
93
+ @connection.queue_event(event)
94
+ when 4
95
+ _serial, depressed, latched, locked, group = payload.unpack("V5")
96
+ keyboard.handle_modifiers(
97
+ depressed: depressed,
98
+ latched: latched,
99
+ locked: locked,
100
+ group: group
101
+ )
102
+ end
103
+ end
52
104
 
53
- object.ack_configure(payload.unpack1("V"))
54
- @connection.flush
105
+ def handle_pointer_event(pointer, opcode, payload)
106
+ case opcode
107
+ when 0
108
+ _serial, surface_id, x, y = payload.unpack("VVl<l<")
109
+ pointer.focus(surface_id)
110
+ queue_pointer_motion(pointer, x, y)
111
+ when 1
112
+ pointer.blur
113
+ when 2
114
+ _time, x, y = payload.unpack("Vl<l<")
115
+ queue_pointer_motion(pointer, x, y)
116
+ when 3
117
+ _serial, _time, button, state = payload.unpack("V4")
118
+ @connection.queue_event(
119
+ type: state == 1 ? :pointer_button_press : :pointer_button_release,
120
+ surface_id: pointer.focused_surface_id,
121
+ button: InputMapper.button(button),
122
+ x: pointer.x,
123
+ y: pointer.y
124
+ )
125
+ when 4
126
+ _time, axis, value = payload.unpack("VVl<")
127
+ @connection.queue_event(
128
+ type: :pointer_axis,
129
+ surface_id: pointer.focused_surface_id,
130
+ axis: { 0 => :vertical, 1 => :horizontal }.fetch(axis, axis),
131
+ value: value / 256.0,
132
+ x: pointer.x,
133
+ y: pointer.y
134
+ )
135
+ end
136
+ end
137
+
138
+ def queue_pointer_motion(pointer, fixed_x, fixed_y)
139
+ pointer.move(fixed_x / 256.0, fixed_y / 256.0)
140
+ @connection.queue_event(
141
+ type: :pointer_motion,
142
+ surface_id: pointer.focused_surface_id,
143
+ x: pointer.x,
144
+ y: pointer.y
145
+ )
55
146
  end
56
147
 
57
148
  def handle_xdg_toplevel_event(object_id, opcode, payload)
@@ -9,9 +9,10 @@ module RBGL
9
9
  BINDINGS = {
10
10
  "wl_compositor" => [:compositor, Compositor, 4],
11
11
  "wl_shm" => [:shm, Shm, 1],
12
- "xdg_wm_base" => [:xdg_wm_base, XdgWmBase, 2]
12
+ "xdg_wm_base" => [:xdg_wm_base, XdgWmBase, 2],
13
+ "wl_seat" => [:seat, Seat, 5]
13
14
  }.freeze
14
- REQUIRED_INTERFACES = BINDINGS.keys.freeze
15
+ REQUIRED_INTERFACES = %w[wl_compositor wl_shm xdg_wm_base].freeze
15
16
 
16
17
  def initialize(connection)
17
18
  @connection = connection
@@ -25,8 +26,9 @@ module RBGL
25
26
  global = globals[interface]
26
27
  next unless global
27
28
 
28
- object_id = registry.bind(global[:name], interface, [global[:version], max_version].min)
29
- bound[slot] = @connection.register_object(klass.new(@connection, object_id))
29
+ version = [global[:version], max_version].min
30
+ object_id = registry.bind(global[:name], interface, version)
31
+ bound[slot] = @connection.register_object(klass.new(@connection, object_id, version: version))
30
32
  end
31
33
  end
32
34
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module Wayland
6
+ module InputMapper
7
+ KEY_NAMES = {
8
+ 1 => :escape,
9
+ 14 => :backspace,
10
+ 15 => :tab,
11
+ 28 => :enter,
12
+ 29 => :left_control,
13
+ 42 => :left_shift,
14
+ 54 => :right_shift,
15
+ 56 => :left_alt,
16
+ 57 => :space,
17
+ 58 => :caps_lock,
18
+ 87 => :f11,
19
+ 88 => :f12,
20
+ 97 => :right_control,
21
+ 100 => :right_alt,
22
+ 102 => :home,
23
+ 103 => :up,
24
+ 104 => :page_up,
25
+ 105 => :left,
26
+ 106 => :right,
27
+ 107 => :end,
28
+ 108 => :down,
29
+ 109 => :page_down,
30
+ 110 => :insert,
31
+ 111 => :delete,
32
+ 125 => :left_meta,
33
+ 126 => :right_meta
34
+ }.merge(
35
+ (2..10).zip(%i[one two three four five six seven eight nine]).to_h,
36
+ { 11 => :zero },
37
+ (16..25).zip(%i[q w e r t y u i o p]).to_h,
38
+ (30..38).zip(%i[a s d f g h j k l]).to_h,
39
+ (44..50).zip(%i[z x c v b n m]).to_h,
40
+ (59..68).zip(%i[f1 f2 f3 f4 f5 f6 f7 f8 f9 f10]).to_h
41
+ ).freeze
42
+
43
+ POINTER_BUTTONS = {
44
+ 0x110 => 1,
45
+ 0x111 => 3,
46
+ 0x112 => 2
47
+ }.freeze
48
+
49
+ module_function
50
+
51
+ def key(code)
52
+ KEY_NAMES.fetch(code, code)
53
+ end
54
+
55
+ def button(code)
56
+ POINTER_BUTTONS.fetch(code, code)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -35,16 +35,28 @@ module RBGL
35
35
  end
36
36
 
37
37
  class WaylandObject
38
- attr_reader :id, :connection
38
+ attr_reader :id, :connection, :version
39
39
 
40
- def initialize(connection, id)
40
+ def initialize(connection, id, version: 1)
41
41
  @connection = connection
42
42
  @id = id
43
+ @version = version
44
+ @destroyed = false
43
45
  end
44
46
 
45
47
  def send_request(opcode, *args)
46
48
  @connection.send_request(@id, opcode, *args)
47
49
  end
50
+
51
+ private
52
+
53
+ def release(opcode, since: 1)
54
+ return if @destroyed
55
+
56
+ send_request(opcode) if @version >= since
57
+ @connection.unregister_object(@id) if @connection.respond_to?(:unregister_object)
58
+ @destroyed = true
59
+ end
48
60
  end
49
61
  end
50
62
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module Wayland
6
+ class Seat < WaylandObject
7
+ POINTER_CAPABILITY = 1
8
+ KEYBOARD_CAPABILITY = 2
9
+
10
+ attr_reader :keyboard, :pointer
11
+
12
+ def handle_capabilities(capabilities)
13
+ update_pointer(capabilities & POINTER_CAPABILITY != 0)
14
+ update_keyboard(capabilities & KEYBOARD_CAPABILITY != 0)
15
+ end
16
+
17
+ def destroy
18
+ @pointer&.destroy
19
+ @keyboard&.destroy
20
+ release(3, since: 5)
21
+ end
22
+
23
+ private
24
+
25
+ def update_pointer(available)
26
+ @pointer ||= create_pointer if available
27
+ return if available || !@pointer
28
+
29
+ @pointer.destroy
30
+ @pointer = nil
31
+ end
32
+
33
+ def update_keyboard(available)
34
+ @keyboard ||= create_keyboard if available
35
+ return if available || !@keyboard
36
+
37
+ @keyboard.destroy
38
+ @keyboard = nil
39
+ end
40
+
41
+ def create_pointer
42
+ object_id = @connection.allocate_id
43
+ send_request(0, Arguments.new_id(object_id))
44
+ @connection.register_object(Pointer.new(@connection, object_id, version: @version))
45
+ end
46
+
47
+ def create_keyboard
48
+ object_id = @connection.allocate_id
49
+ send_request(1, Arguments.new_id(object_id))
50
+ @connection.register_object(Keyboard.new(@connection, object_id, version: @version))
51
+ end
52
+ end
53
+
54
+ class Keyboard < WaylandObject
55
+ attr_reader :focused_surface_id, :keymap, :keymap_format, :modifiers
56
+
57
+ def initialize(...)
58
+ super
59
+ @modifiers = {}
60
+ end
61
+
62
+ def focus(surface_id)
63
+ @focused_surface_id = surface_id
64
+ end
65
+
66
+ def blur
67
+ @focused_surface_id = nil
68
+ end
69
+
70
+ def handle_keymap(format, size, io)
71
+ raise GUI::BackendUnavailable, "Wayland keymap event did not include a file descriptor" unless io
72
+
73
+ @keymap_format = { 0 => :none, 1 => :xkb_v1 }.fetch(format, format)
74
+ @keymap = io.read(size)&.delete_suffix("\x00")
75
+ ensure
76
+ io&.close
77
+ end
78
+
79
+ def handle_modifiers(depressed:, latched:, locked:, group:)
80
+ @modifiers = {
81
+ depressed: depressed,
82
+ latched: latched,
83
+ locked: locked,
84
+ group: group
85
+ }
86
+ end
87
+
88
+ def destroy
89
+ release(0, since: 3)
90
+ end
91
+ end
92
+
93
+ class Pointer < WaylandObject
94
+ attr_reader :focused_surface_id, :x, :y
95
+
96
+ def focus(surface_id)
97
+ @focused_surface_id = surface_id
98
+ end
99
+
100
+ def blur
101
+ @focused_surface_id = nil
102
+ end
103
+
104
+ def move(x, y)
105
+ @x = x
106
+ @y = y
107
+ end
108
+
109
+ def destroy
110
+ release(1, since: 3)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -4,9 +4,9 @@ module RBGL
4
4
  module GUI
5
5
  module Wayland
6
6
  class Shm < WaylandObject
7
- def create_pool(fd, size)
7
+ def create_pool(file, size)
8
8
  pool_id = @connection.allocate_id
9
- @connection.send_request_with_fd(@id, 0, Arguments.new_id(pool_id), Arguments.int(size), fd)
9
+ @connection.send_request_with_fd(@id, 0, Arguments.new_id(pool_id), Arguments.int(size), file)
10
10
  @connection.register_object(ShmPool.new(@connection, pool_id))
11
11
  end
12
12
  end
@@ -17,7 +17,8 @@ module RBGL
17
17
  format_val = case format
18
18
  when :argb8888 then 0
19
19
  when :xrgb8888 then 1
20
- else 0
20
+ else
21
+ raise ArgumentError, "Unsupported Wayland SHM format: #{format}"
21
22
  end
22
23
  send_request(
23
24
  0,
@@ -67,10 +68,10 @@ module RBGL
67
68
  finalize_destroy if @destroy_requested
68
69
  end
69
70
 
70
- def destroy
71
- return if @destroyed || @destroy_requested
71
+ def destroy(force: false)
72
+ return if @destroyed || (@destroy_requested && !force)
72
73
 
73
- if @busy
74
+ if @busy && !force
74
75
  @destroy_requested = true
75
76
  else
76
77
  finalize_destroy
@@ -119,12 +120,19 @@ module RBGL
119
120
  @wl_buffer.id
120
121
  end
121
122
 
122
- def destroy
123
+ def destroy(force: false)
123
124
  return if @destroyed
124
125
 
125
126
  @destroy_requested = true
126
- @wl_buffer.destroy
127
- finalize_destroy unless @wl_buffer.busy?
127
+ begin
128
+ force ? @wl_buffer.destroy(force: true) : @wl_buffer.destroy
129
+ ensure
130
+ finalize_destroy if force || !@wl_buffer.busy?
131
+ end
132
+ end
133
+
134
+ def destroyed?
135
+ @destroyed
128
136
  end
129
137
 
130
138
  private
@@ -136,9 +144,12 @@ module RBGL
136
144
  def finalize_destroy
137
145
  return if @destroyed
138
146
 
139
- @pool.destroy
140
- @file.close unless @file.closed?
141
- @destroyed = true
147
+ begin
148
+ @pool.destroy
149
+ ensure
150
+ @file.close unless @file.closed?
151
+ @destroyed = true
152
+ end
142
153
  end
143
154
  end
144
155
  end
@@ -16,6 +16,15 @@ module RBGL
16
16
  end
17
17
 
18
18
  class XdgSurface < WaylandObject
19
+ def initialize(connection, id)
20
+ super
21
+ @configured = false
22
+ end
23
+
24
+ def configured?
25
+ @configured
26
+ end
27
+
19
28
  def get_toplevel
20
29
  toplevel_id = @connection.allocate_id
21
30
  send_request(1, Arguments.new_id(toplevel_id))
@@ -26,6 +35,12 @@ module RBGL
26
35
  send_request(4, Arguments.uint(serial))
27
36
  end
28
37
 
38
+ def handle_configure(serial)
39
+ ack_configure(serial)
40
+ @connection.flush
41
+ @configured = true
42
+ end
43
+
29
44
  def destroy
30
45
  send_request(0)
31
46
  end
@@ -4,4 +4,5 @@ require_relative "protocol_objects/arguments"
4
4
  require_relative "protocol_objects/display"
5
5
  require_relative "protocol_objects/surface"
6
6
  require_relative "protocol_objects/shm"
7
+ require_relative "protocol_objects/seat"
7
8
  require_relative "protocol_objects/xdg_shell"
@@ -5,39 +5,60 @@ module RBGL
5
5
  class Window
6
6
  class RenderLoop
7
7
  DEFAULT_TIME_SOURCE = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
8
+ DEFAULT_SLEEPER = ->(duration) { sleep(duration) }
9
+ FPS_WINDOW_SECONDS = 1.0
8
10
 
9
11
  attr_reader :fps, :dropped_frames
10
12
 
11
- def initialize(time_source: DEFAULT_TIME_SOURCE)
13
+ def initialize(time_source: DEFAULT_TIME_SOURCE, sleeper: DEFAULT_SLEEPER, target_fps: 60)
12
14
  @time_source = time_source
15
+ @sleeper = sleeper
16
+ @frame_interval = target_fps && (1.0 / validate_target_fps(target_fps))
13
17
  @fps = 0
14
18
  @dropped_frames = 0
15
19
  @running = false
20
+ @frame_times = []
16
21
  end
17
22
 
18
23
  def run(backend:, context:, process_events:, &frame_callback)
19
- @running = true
20
- start_time = @time_source.call
21
- last_time = start_time
22
- frame_count = 0
24
+ start
23
25
 
24
26
  while @running && !backend.should_close?
25
- current_time = @time_source.call
26
- delta_time = current_time - last_time
27
- last_time = current_time
28
-
29
- process_events.call
30
- break if stop_requested?(backend)
31
- frame_callback&.call(context, delta_time)
32
- break if stop_requested?(backend)
33
- record_present_result(backend.present(context.framebuffer))
34
-
35
- frame_count += 1
36
- elapsed = current_time - start_time
37
- @fps = frame_count / elapsed if elapsed.positive?
27
+ frame_finished_at = step(backend:, context:, process_events:, &frame_callback)
28
+ break unless frame_finished_at
29
+
30
+ throttle(@next_frame_deadline, frame_finished_at)
31
+ @next_frame_deadline = advance_deadline(@next_frame_deadline, frame_finished_at)
38
32
  end
39
33
  end
40
34
 
35
+ def start(now: nil)
36
+ @running = true
37
+ @last_time = now.nil? ? @time_source.call : Float(now)
38
+ @next_frame_deadline = @frame_interval && (@last_time + @frame_interval)
39
+ @frame_times.clear
40
+ end
41
+
42
+ def step(backend:, context:, process_events:, now: nil, &frame_callback)
43
+ start(now:) if @last_time.nil?
44
+ return false unless @running
45
+ return false if backend.should_close?
46
+
47
+ frame_started_at = now.nil? ? @time_source.call : Float(now)
48
+ delta_time = frame_started_at - @last_time
49
+ @last_time = frame_started_at
50
+
51
+ process_events.call
52
+ return false if stop_requested?(backend)
53
+ frame_callback&.call(context, delta_time)
54
+ return false if stop_requested?(backend)
55
+
56
+ record_present_result(backend.present(context.framebuffer))
57
+ @last_frame_finished_at = now.nil? ? @time_source.call : frame_started_at
58
+ record_frame(@last_frame_finished_at)
59
+ @last_frame_finished_at
60
+ end
61
+
41
62
  def record_present_result(result)
42
63
  @dropped_frames += 1 if result == false
43
64
  result
@@ -49,6 +70,38 @@ module RBGL
49
70
 
50
71
  private
51
72
 
73
+ def record_frame(timestamp)
74
+ @frame_times << timestamp
75
+ cutoff = timestamp - FPS_WINDOW_SECONDS
76
+ @frame_times.shift while @frame_times.length > 2 && @frame_times.first < cutoff
77
+ return @fps = 0 if @frame_times.length < 2
78
+
79
+ elapsed = @frame_times.last - @frame_times.first
80
+ @fps = elapsed.positive? ? (@frame_times.length - 1) / elapsed : 0
81
+ end
82
+
83
+ def throttle(deadline, frame_finished_at)
84
+ return unless deadline
85
+
86
+ remaining = deadline - frame_finished_at
87
+ @sleeper.call(remaining) if remaining.positive?
88
+ end
89
+
90
+ def advance_deadline(deadline, frame_finished_at)
91
+ return unless deadline
92
+
93
+ [deadline, frame_finished_at].max + @frame_interval
94
+ end
95
+
96
+ def validate_target_fps(target_fps)
97
+ fps = Float(target_fps)
98
+ return fps if fps.positive?
99
+
100
+ raise ArgumentError, "target_fps must be positive or nil"
101
+ rescue ArgumentError, TypeError
102
+ raise ArgumentError, "target_fps must be positive or nil"
103
+ end
104
+
52
105
  def stop_requested?(backend)
53
106
  !@running || backend.should_close?
54
107
  end