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
@@ -17,7 +17,7 @@ module RBGL
17
17
  def encode(framebuffer)
18
18
  width = framebuffer.width
19
19
  height = framebuffer.height
20
- row_size = ((24 * width + 31) / 32) * 4
20
+ row_size = (((24 * width) + 31) / 32) * 4
21
21
  pixel_data_size = row_size * height
22
22
  file_size = 54 + pixel_data_size
23
23
 
@@ -51,7 +51,7 @@ module RBGL
51
51
  bytes = framebuffer.get_pixel(x, y).to_bytes
52
52
  row << [bytes[2], bytes[1], bytes[0]].pack("CCC")
53
53
  end
54
- row << "\x00" * (row_size - width * 3)
54
+ row << ("\x00" * (row_size - (width * 3)))
55
55
  pixels << row
56
56
  end
57
57
 
@@ -10,6 +10,13 @@ module RBGL
10
10
  PpmWriter.new(binary: ppm_mode == :binary)
11
11
  when :bmp
12
12
  BmpWriter.new
13
+ when :png
14
+ begin
15
+ require "tessel"
16
+ rescue LoadError
17
+ raise ArgumentError, "format: :png requires the tessel gem"
18
+ end
19
+ PngWriter.new
13
20
  else
14
21
  raise ArgumentError, "Unsupported file backend format: #{format}"
15
22
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class FileBackend
6
+ class PngWriter < FrameWriter
7
+ def extension
8
+ "png"
9
+ end
10
+
11
+ def write(filename, framebuffer)
12
+ image = Tessel::Image.from_rgba(framebuffer.width, framebuffer.height, framebuffer.to_rgba_bytes)
13
+ image.write(filename)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -5,6 +5,7 @@ module RBGL
5
5
  class FileBackend < Backend
6
6
  class PpmWriter < FrameWriter
7
7
  def initialize(binary:)
8
+ super()
8
9
  @binary = binary
9
10
  end
10
11
 
@@ -1,24 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require_relative "file_backend/frame_writer"
4
5
  require_relative "file_backend/ppm_writer"
5
6
  require_relative "file_backend/bmp_writer"
7
+ require_relative "file_backend/png_writer"
6
8
 
7
9
  module RBGL
8
10
  module GUI
9
11
  class FileBackend < Backend
10
- SUPPORTED_FORMATS = %i[ppm bmp].freeze
12
+ SUPPORTED_FORMATS = %i[ppm bmp png].freeze
11
13
  PPM_MODES = %i[ascii binary].freeze
12
14
 
13
- def initialize(width, height, title = "RBGL", format: :ppm, ppm_mode: :ascii, output_dir: ".")
15
+ attr_reader :max_frames, :output_dir
16
+
17
+ def initialize(width, height, title = "RBGL", format: :ppm, ppm_mode: :ascii, output_dir: ".", max_frames: 1)
14
18
  super(width, height, title)
15
19
  @format = normalize_format(format)
16
20
  @ppm_mode = normalize_ppm_mode(ppm_mode, @format)
17
21
  @writer = FrameWriter.build(@format, ppm_mode: @ppm_mode)
18
- @output_dir = output_dir
22
+ @output_dir = File.expand_path(output_dir)
23
+ FileUtils.mkdir_p(@output_dir)
19
24
  @frame_count = 0
20
25
  @should_close = false
21
- @max_frames = nil
26
+ self.max_frames = max_frames
22
27
  end
23
28
 
24
29
  def present(framebuffer)
@@ -43,10 +48,12 @@ module RBGL
43
48
  @should_close = true
44
49
  end
45
50
 
46
- def set_max_frames(count)
47
- @max_frames = count
51
+ def max_frames=(count)
52
+ @max_frames = normalize_max_frames(count)
48
53
  end
49
54
 
55
+ alias set_max_frames max_frames=
56
+
50
57
  private
51
58
 
52
59
  def normalize_format(format)
@@ -63,6 +70,17 @@ module RBGL
63
70
 
64
71
  raise ArgumentError, "PPM mode is only supported with :ppm format"
65
72
  end
73
+
74
+ def normalize_max_frames(count)
75
+ return nil if count.nil?
76
+
77
+ normalized = Integer(count)
78
+ return normalized if normalized.positive?
79
+
80
+ raise ArgumentError, "max_frames must be a positive integer or nil"
81
+ rescue TypeError
82
+ raise ArgumentError, "max_frames must be a positive integer or nil"
83
+ end
66
84
  end
67
85
  end
68
86
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "connection"
4
+ require "tempfile"
4
5
 
5
6
  module RBGL
6
7
  module GUI
@@ -11,9 +12,17 @@ module RBGL
11
12
 
12
13
  def initialize(width, height, title = "RBGL", env: ENV, roundtrip_timeout: Connection::DEFAULT_ROUNDTRIP_TIMEOUT)
13
14
  super(width, height, title)
15
+ @window = nil
16
+ @retired_buffers = []
14
17
  @connection = Connection.new(env: env, roundtrip_timeout: roundtrip_timeout)
15
- @windows = {}
16
18
  setup_window(width, height, title)
19
+ rescue StandardError => error
20
+ begin
21
+ close
22
+ rescue StandardError
23
+ nil
24
+ end
25
+ raise error
17
26
  end
18
27
 
19
28
  private def setup_window(w, h, t)
@@ -22,42 +31,52 @@ module RBGL
22
31
  toplevel = xdg_surface.get_toplevel
23
32
  toplevel.set_title(t)
24
33
 
25
- buffers = create_shm_buffers(w, h)
26
- shm_buffer = buffers.first
27
-
28
- surface.attach(shm_buffer, 0, 0)
29
- shm_buffer.mark_in_use
30
- surface.commit
31
- @connection.flush
32
-
33
- handle = surface.id
34
- @windows[handle] = {
34
+ @window = {
35
35
  surface: surface,
36
36
  xdg_surface: xdg_surface,
37
37
  toplevel: toplevel,
38
- buffers: buffers,
39
- shm_buffer: shm_buffer,
38
+ buffers: [],
39
+ shm_buffer: nil,
40
40
  width: w,
41
41
  height: h,
42
42
  should_close: false
43
43
  }
44
44
 
45
- @handle = handle
45
+ surface.commit
46
+ @connection.flush
47
+ wait_for_initial_configure(xdg_surface)
48
+
49
+ buffers = create_shm_buffers(w, h)
50
+ @window[:buffers] = buffers
51
+ @window[:shm_buffer] = buffers.first
46
52
  end
47
53
 
48
54
  def present(framebuffer)
49
- return false unless @handle
55
+ return false unless @window
56
+
57
+ present_bytes(convert_to_wayland_format(framebuffer), framebuffer.width, framebuffer.height)
58
+ end
59
+
60
+ def set_pixels(buffer, width, height)
61
+ return false unless @window
50
62
 
51
- window = @windows[@handle]
63
+ bytes = validate_rgba_buffer(buffer, width, height)
64
+ present_bytes(rgba_to_wayland_bytes(bytes), width, height)
65
+ end
66
+
67
+ private def present_bytes(buffer, width, height)
68
+ window = @window
52
69
  return false unless window
70
+ unless width == window[:width] && height == window[:height]
71
+ raise ArgumentError, "Framebuffer dimensions must match the Wayland window"
72
+ end
53
73
 
54
74
  buffer_object = wait_for_available_buffer(window)
55
75
  return false unless buffer_object
56
76
 
57
- buffer = convert_to_wayland_format(framebuffer)
58
77
  buffer_object.write(buffer)
59
78
 
60
- window[:surface].damage(0, 0, framebuffer.width, framebuffer.height)
79
+ window[:surface].damage(0, 0, width, height)
61
80
  window[:surface].attach(buffer_object, 0, 0)
62
81
  buffer_object.mark_in_use
63
82
  window[:shm_buffer] = buffer_object
@@ -71,12 +90,9 @@ module RBGL
71
90
  end
72
91
 
73
92
  def resize(width, height)
74
- super
75
- return unless @handle
76
-
77
- window = @windows[@handle]
78
- return unless window
79
- return if window[:width] == width && window[:height] == height
93
+ window = @window
94
+ return super unless window
95
+ return super if window[:width] == width && window[:height] == height
80
96
 
81
97
  old_buffers = window.fetch(:buffers, [window[:shm_buffer]].compact)
82
98
  new_buffers = create_shm_buffers(width, height)
@@ -84,28 +100,39 @@ module RBGL
84
100
  window[:shm_buffer] = new_buffers.first
85
101
  window[:width] = width
86
102
  window[:height] = height
103
+ super
104
+ (@retired_buffers ||= []).concat(old_buffers)
87
105
  old_buffers.each(&:destroy)
106
+ @retired_buffers.reject! { |buffer| buffer.respond_to?(:destroyed?) && buffer.destroyed? }
88
107
  end
89
108
 
90
109
  def should_close?
91
- return false unless @handle
92
-
93
- @windows[@handle]&.[](:should_close) || false
110
+ @window.nil? || @window[:should_close]
94
111
  end
95
112
 
96
113
  def close
97
- return unless @handle
98
-
99
- window = @windows[@handle]
100
- return unless window
114
+ window = @window
115
+ operations = []
116
+ if window
117
+ window[:should_close] = true
118
+ operations << -> { window[:toplevel].destroy }
119
+ operations << -> { window[:xdg_surface].destroy }
120
+ operations << -> { window[:surface].destroy }
121
+ buffers = window.fetch(:buffers, [window[:shm_buffer]].compact) + Array(@retired_buffers)
122
+ operations.concat(buffers.uniq.map { |buffer| -> { buffer.destroy(force: true) } })
123
+ operations << -> { @connection.flush }
124
+ end
125
+ operations << -> { @connection&.close }
101
126
 
102
- window[:should_close] = true
103
- window[:toplevel].destroy
104
- window[:xdg_surface].destroy
105
- window[:surface].destroy
106
- window.fetch(:buffers, [window[:shm_buffer]].compact).each(&:destroy)
107
- @windows.delete(@handle)
108
- @handle = nil
127
+ error = nil
128
+ operations.each do |operation|
129
+ operation.call
130
+ rescue StandardError => cleanup_error
131
+ error ||= cleanup_error
132
+ end
133
+ @window = nil
134
+ @retired_buffers = []
135
+ raise error if error
109
136
  end
110
137
 
111
138
  private
@@ -114,8 +141,19 @@ module RBGL
114
141
  framebuffer.to_bgra_bytes
115
142
  end
116
143
 
144
+ def rgba_to_wayland_bytes(buffer)
145
+ output = String.new(capacity: buffer.bytesize, encoding: Encoding::BINARY)
146
+ byte_index = 0
147
+ while byte_index < buffer.bytesize
148
+ output << buffer.getbyte(byte_index + 2) << buffer.getbyte(byte_index + 1) <<
149
+ buffer.getbyte(byte_index) << buffer.getbyte(byte_index + 3)
150
+ byte_index += 4
151
+ end
152
+ output
153
+ end
154
+
117
155
  def convert_event(raw)
118
- window = window_for_event(raw[:object_id])
156
+ window = window_for_event(raw[:surface_id] || raw[:object_id])
119
157
  return nil unless window
120
158
 
121
159
  case raw[:type]
@@ -127,14 +165,23 @@ module RBGL
127
165
  height = raw[:height]
128
166
  return nil unless width.positive? && height.positive?
129
167
 
130
- window[:width] = width
131
- window[:height] = height
132
168
  Event.new(:resize, width: width, height: height)
169
+ when :key_press, :key_release
170
+ Event.new(raw[:type], key: raw[:key], keycode: raw[:keycode], modifiers: raw[:modifiers])
171
+ when :pointer_motion
172
+ Event.new(:mouse_move, x: raw[:x], y: raw[:y])
173
+ when :pointer_button_press
174
+ Event.new(:mouse_press, button: raw[:button], x: raw[:x], y: raw[:y])
175
+ when :pointer_button_release
176
+ Event.new(:mouse_release, button: raw[:button], x: raw[:x], y: raw[:y])
177
+ when :pointer_axis
178
+ Event.new(:mouse_scroll, x: raw[:x], y: raw[:y], axis: raw[:axis], value: raw[:value])
133
179
  end
134
180
  end
135
181
 
136
182
  def window_for_event(object_id)
137
- @windows.values.find { |window| window[:toplevel].id == object_id }
183
+ return unless @window
184
+ return @window if @window[:toplevel].id == object_id || @window[:surface].id == object_id
138
185
  end
139
186
 
140
187
  def next_available_buffer(window)
@@ -174,32 +221,57 @@ module RBGL
174
221
  end
175
222
 
176
223
  def create_shm_buffers(width, height, count = 2)
177
- Array.new(count) { create_shm_buffer(width, height) }
224
+ buffers = []
225
+ count.times { buffers << create_shm_buffer(width, height) }
226
+ buffers
227
+ rescue StandardError => error
228
+ buffers&.each do |buffer|
229
+ begin
230
+ buffer.destroy(force: true)
231
+ rescue StandardError
232
+ nil
233
+ end
234
+ end
235
+ raise error
178
236
  end
179
237
 
180
238
  def create_shm_buffer(width, height)
181
239
  size = width * height * 4
182
240
 
183
241
  file = create_anonymous_file(size)
184
- pool = @connection.shm.create_pool(file.fileno, size)
185
- buffer = pool.create_buffer(0, width, height, width * 4, :argb8888)
242
+ pool = @connection.shm.create_pool(file, size)
243
+ buffer = pool.create_buffer(0, width, height, width * 4, :xrgb8888)
186
244
 
187
245
  ShmBuffer.new(file, pool, buffer)
246
+ rescue StandardError => error
247
+ begin
248
+ buffer&.destroy(force: true)
249
+ rescue StandardError
250
+ nil
251
+ end
252
+ begin
253
+ pool&.destroy
254
+ rescue StandardError
255
+ nil
256
+ end
257
+ file&.close unless file&.closed?
258
+ raise error
188
259
  end
189
260
 
190
261
  def create_anonymous_file(size)
191
- name = "rbgl-#{Process.pid}-#{rand(10000)}"
192
- path = "/dev/shm/#{name}"
193
-
194
- file = File.open(path, File::RDWR | File::CREAT | File::EXCL, 0o600)
262
+ file = Tempfile.new("rbgl-wayland")
263
+ file.binmode
264
+ file.chmod(0o600)
195
265
  file.truncate(size)
196
- File.unlink(path)
266
+ file.unlink
197
267
  file
198
- rescue Errno::ENOENT
199
- require "tempfile"
200
- tmpfile = Tempfile.new("rbgl")
201
- tmpfile.truncate(size)
202
- tmpfile
268
+ end
269
+
270
+ def wait_for_initial_configure(xdg_surface)
271
+ configured = @connection.wait_until { xdg_surface.configured? }
272
+ return if configured
273
+
274
+ raise GUI::BackendUnavailable, "Wayland compositor did not configure the surface"
203
275
  end
204
276
 
205
277
  end
@@ -8,16 +8,11 @@ module RBGL
8
8
  result = String.new
9
9
 
10
10
  args.each do |arg|
11
- case arg
12
- when TypedArgument
13
- result << pack_typed_argument(arg)
14
- when Integer
15
- result << [arg].pack("V")
16
- when String
17
- result << pack_string(arg)
18
- when Float
19
- result << [(arg * 256).to_i].pack("V")
11
+ unless arg.is_a?(TypedArgument)
12
+ raise ArgumentError, "Wayland requests require typed arguments"
20
13
  end
14
+
15
+ result << pack_typed_argument(arg)
21
16
  end
22
17
 
23
18
  result
@@ -45,8 +40,9 @@ module RBGL
45
40
  end
46
41
 
47
42
  def pack_string(value)
48
- length = value.bytesize + 1
49
- [length].pack("V") + value + "\x00" + ("\x00" * ((4 - length % 4) % 4))
43
+ bytes = value.to_s.encode(Encoding::UTF_8).b
44
+ length = bytes.bytesize + 1
45
+ [length].pack("V") + bytes + "\x00" + ("\x00" * ((4 - (length % 4)) % 4))
50
46
  end
51
47
  end
52
48
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "socket"
4
+ require "io/wait"
4
5
  require_relative "protocol_objects"
5
6
  require_relative "codec"
6
7
  require_relative "event_dispatcher"
@@ -12,8 +13,9 @@ module RBGL
12
13
  class Connection
13
14
  DEFAULT_ROUNDTRIP_TIMEOUT = 1.0
14
15
  ROUNDTRIP_POLL_INTERVAL = 0.01
16
+ READ_CHUNK_SIZE = 16_384
15
17
 
16
- attr_reader :compositor, :shm, :xdg_wm_base, :registry, :globals
18
+ attr_reader :compositor, :shm, :xdg_wm_base, :seat, :registry, :globals
17
19
 
18
20
  def initialize(env: ENV, socket_path: nil, roundtrip_timeout: DEFAULT_ROUNDTRIP_TIMEOUT)
19
21
  @env = env
@@ -23,6 +25,9 @@ module RBGL
23
25
  @next_id = 2
24
26
  @globals = {}
25
27
  @pending_events = []
28
+ @received_fds = []
29
+ @read_buffer = String.new(encoding: Encoding::BINARY)
30
+ @closed = false
26
31
 
27
32
  @display = Display.new(self)
28
33
  register_object(@display)
@@ -32,6 +37,13 @@ module RBGL
32
37
  roundtrip
33
38
 
34
39
  bind_globals
40
+ rescue StandardError => error
41
+ begin
42
+ close
43
+ rescue StandardError
44
+ nil
45
+ end
46
+ raise error
35
47
  end
36
48
 
37
49
  def allocate_id
@@ -49,25 +61,42 @@ module RBGL
49
61
  @objects[object_id]
50
62
  end
51
63
 
64
+ def unregister_object(object_id)
65
+ @objects.delete(object_id)
66
+ end
67
+
52
68
  def store_global(interface, name:, version:)
53
69
  @globals[interface] = { name: name, version: version }
54
70
  end
55
71
 
72
+ def remove_global(name)
73
+ @globals.delete_if { |_interface, global| global[:name] == name }
74
+ end
75
+
76
+ def consume_received_fd
77
+ @received_fds.shift
78
+ end
79
+
56
80
  def queue_event(event)
57
81
  @pending_events << event
58
82
  end
59
83
 
60
84
  def send_request(object_id, opcode, *args)
61
85
  payload = pack_args(args)
62
- header = [object_id, (payload.bytesize + 8) << 16 | opcode].pack("VV")
86
+ header = [object_id, ((payload.bytesize + 8) << 16) | opcode].pack("VV")
63
87
  @socket.write(header + payload)
64
88
  end
65
89
 
66
90
  def send_request_with_fd(object_id, opcode, *args, fd)
91
+ io = fd.respond_to?(:to_io) ? fd.to_io : fd
92
+ unless io.is_a?(IO)
93
+ raise ArgumentError, "Wayland file descriptor arguments must be IO objects"
94
+ end
95
+
67
96
  payload = pack_args(args)
68
- header = [object_id, (payload.bytesize + 8) << 16 | opcode].pack("VV")
97
+ header = [object_id, ((payload.bytesize + 8) << 16) | opcode].pack("VV")
69
98
 
70
- @socket.sendmsg(header + payload, 0, nil, Socket::AncillaryData.unix_rights(fd))
99
+ @socket.sendmsg(header + payload, 0, nil, Socket::AncillaryData.unix_rights(io))
71
100
  end
72
101
 
73
102
  def flush
@@ -83,28 +112,16 @@ module RBGL
83
112
  end
84
113
 
85
114
  def pump_events(timeout: nil)
86
- ready = IO.select([@socket], nil, nil, timeout)
87
- return 0 unless ready
88
-
89
- loop do
90
- header = @socket.read(8)
91
- break unless header && header.bytesize == 8
92
-
93
- object_id, size_and_opcode = header.unpack("VV")
94
- size = size_and_opcode >> 16
95
- opcode = size_and_opcode & 0xFFFF
96
- payload = size > 8 ? @socket.read(size - 8) : ""
97
-
98
- handle_event(object_id, opcode, payload)
99
- break unless IO.select([@socket], nil, nil, 0)
100
- end
115
+ read_from_socket(timeout) unless complete_message?
116
+ drain_messages
117
+ read_from_socket(0) if @socket.wait_readable(0)
118
+ drain_messages
101
119
 
102
120
  @pending_events.length
103
121
  end
104
122
 
105
123
  def roundtrip
106
124
  callback = @display.sync
107
- @objects[callback.id] = callback
108
125
  flush
109
126
  deadline = monotonic_time + @roundtrip_timeout
110
127
 
@@ -115,6 +132,43 @@ module RBGL
115
132
  end
116
133
  end
117
134
 
135
+ def wait_until(timeout: @roundtrip_timeout)
136
+ deadline = monotonic_time + timeout
137
+
138
+ until yield
139
+ return false if monotonic_time >= deadline
140
+
141
+ pump_events(timeout: roundtrip_poll_interval(deadline))
142
+ end
143
+
144
+ true
145
+ end
146
+
147
+ def close
148
+ return if @closed
149
+
150
+ error = nil
151
+ @received_fds&.each do |fd|
152
+ fd.close unless fd.closed?
153
+ rescue StandardError => close_error
154
+ error ||= close_error
155
+ end
156
+ @received_fds&.reject!(&:closed?)
157
+
158
+ begin
159
+ @socket&.close unless @socket&.closed?
160
+ rescue StandardError => close_error
161
+ error ||= close_error
162
+ end
163
+
164
+ @closed = @received_fds.to_a.empty? && (!@socket || @socket.closed?)
165
+ raise error if error
166
+ end
167
+
168
+ def closed?
169
+ @closed || !@socket || @socket.closed?
170
+ end
171
+
118
172
  private
119
173
 
120
174
  def resolve_socket_path(socket_path = nil)
@@ -137,11 +191,74 @@ module RBGL
137
191
  event_dispatcher.handle_event(object_id, opcode, payload)
138
192
  end
139
193
 
194
+ def complete_message?
195
+ return false if @read_buffer.bytesize < 8
196
+
197
+ size = @read_buffer.byteslice(4, 4).unpack1("V") >> 16
198
+ raise GUI::BackendUnavailable, "Invalid Wayland message size: #{size}" if size < 8
199
+
200
+ @read_buffer.bytesize >= size
201
+ end
202
+
203
+ def drain_messages
204
+ while complete_message?
205
+ object_id, size_and_opcode = @read_buffer.byteslice(0, 8).unpack("VV")
206
+ size = size_and_opcode >> 16
207
+ opcode = size_and_opcode & 0xFFFF
208
+ message = @read_buffer.slice!(0, size)
209
+ handle_event(object_id, opcode, message.byteslice(8, size - 8))
210
+ end
211
+ end
212
+
213
+ def read_from_socket(timeout)
214
+ return false unless @socket.wait_readable(timeout)
215
+
216
+ read_any = false
217
+ loop do
218
+ message = receive_message
219
+ break if message == :wait_readable
220
+
221
+ chunk, ancillary_data = message
222
+ raise GUI::BackendUnavailable, "Wayland compositor closed the connection" if chunk.nil?
223
+
224
+ @read_buffer << chunk
225
+ ancillary_data.each do |control|
226
+ @received_fds.concat(control.unix_rights)
227
+ rescue TypeError
228
+ next
229
+ end
230
+ read_any = true
231
+ end
232
+ read_any
233
+ end
234
+
235
+ def receive_message
236
+ unless @socket.respond_to?(:recvmsg_nonblock)
237
+ chunk = @socket.read_nonblock(READ_CHUNK_SIZE, exception: false)
238
+ return :wait_readable if chunk == :wait_readable
239
+
240
+ return [chunk, []]
241
+ end
242
+
243
+ result = @socket.recvmsg_nonblock(
244
+ READ_CHUNK_SIZE,
245
+ 0,
246
+ nil,
247
+ scm_rights: true,
248
+ exception: false
249
+ )
250
+ return :wait_readable if result == :wait_readable
251
+ return [nil, []] if !result || result[0].empty?
252
+
253
+ [result[0], result.drop(3)]
254
+ end
255
+
140
256
  def bind_globals
141
257
  bound = global_binder.bind!(@registry, @globals)
142
258
  @compositor = bound[:compositor]
143
259
  @shm = bound[:shm]
144
260
  @xdg_wm_base = bound[:xdg_wm_base]
261
+ @seat = bound[:seat]
145
262
  flush
146
263
  roundtrip
147
264
  end