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.
- checksums.yaml +4 -4
- data/.codespellignore +4 -0
- data/.rubocop.yml +3 -0
- data/.yamllint +16 -0
- data/CHANGELOG.md +11 -1
- data/README.md +130 -74
- data/Rakefile +7 -0
- data/bench/renderer_bench.rb +283 -0
- data/docs/.nojekyll +1 -0
- data/docs/index.html +38 -0
- data/examples/array_test.rb +1 -1
- data/examples/color_test.rb +1 -1
- data/examples/cube_spinning.rb +1 -1
- data/examples/gradient.rb +1 -1
- data/examples/multi_return_test.rb +1 -1
- data/examples/plasma.rb +1 -1
- data/examples/sphere_raymarch.rb +1 -1
- data/examples/triangle_window.rb +1 -1
- data/exe/rbgl +42 -0
- data/lib/rbgl/cli/doctor.rb +155 -0
- data/lib/rbgl/engine/buffer.rb +55 -9
- data/lib/rbgl/engine/clip_space_clipper.rb +23 -3
- data/lib/rbgl/engine/context.rb +23 -6
- data/lib/rbgl/engine/framebuffer.rb +146 -41
- data/lib/rbgl/engine/immutable_color.rb +32 -0
- data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +100 -10
- data/lib/rbgl/engine/rasterizer/line_renderer.rb +30 -14
- data/lib/rbgl/engine/rasterizer/point_renderer.rb +3 -3
- data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +72 -23
- data/lib/rbgl/engine/rasterizer.rb +3 -20
- data/lib/rbgl/engine/shader/base_shader.rb +3 -7
- data/lib/rbgl/engine/shader/builtins.rb +13 -10
- data/lib/rbgl/engine/shader/dynamic_data.rb +18 -10
- data/lib/rbgl/engine/texture.rb +74 -50
- data/lib/rbgl/engine.rb +3 -1
- data/lib/rbgl/gui/backend.rb +12 -7
- data/lib/rbgl/gui/backend_factory.rb +7 -3
- data/lib/rbgl/gui/cocoa/backend.rb +50 -17
- data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
- data/lib/rbgl/gui/event.rb +4 -2
- data/lib/rbgl/gui/file_backend/bmp_writer.rb +2 -2
- data/lib/rbgl/gui/file_backend/frame_writer.rb +7 -0
- data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
- data/lib/rbgl/gui/file_backend/ppm_writer.rb +1 -0
- data/lib/rbgl/gui/file_backend.rb +24 -6
- data/lib/rbgl/gui/wayland/backend.rb +127 -55
- data/lib/rbgl/gui/wayland/codec.rb +7 -11
- data/lib/rbgl/gui/wayland/connection.rb +137 -20
- data/lib/rbgl/gui/wayland/event_dispatcher.rb +103 -12
- data/lib/rbgl/gui/wayland/global_binder.rb +6 -4
- data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
- data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +14 -2
- data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
- data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +23 -12
- data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +15 -0
- data/lib/rbgl/gui/wayland/protocol_objects.rb +1 -0
- data/lib/rbgl/gui/window/render_loop.rb +71 -18
- data/lib/rbgl/gui/window.rb +39 -8
- data/lib/rbgl/gui/x11/atom_cache.rb +4 -1
- data/lib/rbgl/gui/x11/backend.rb +66 -28
- data/lib/rbgl/gui/x11/connection.rb +216 -86
- data/lib/rbgl/gui/x11/event_parser.rb +13 -2
- data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
- data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
- data/lib/rbgl/gui/x11/request_encoder.rb +29 -28
- data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
- data/lib/rbgl/gui/x11/transport.rb +52 -2
- data/lib/rbgl/gui/x11/x_authority.rb +107 -0
- data/lib/rbgl/gui.rb +2 -5
- data/lib/rbgl/version.rb +1 -1
- metadata +38 -24
- data/examples/chemical_heartbeat.rb +0 -166
- data/examples/dark_transit.rb +0 -166
- data/examples/fractured_orb.rb +0 -428
- data/examples/fractured_orb_rb.rb +0 -598
- data/examples/hexagonal_flow.rb +0 -333
- data/examples/teapot.rb +0 -362
- 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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
39
|
-
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
|
-
|
|
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 @
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
75
|
-
return unless
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
@windows[@handle]&.[](:should_close) || false
|
|
110
|
+
@window.nil? || @window[:should_close]
|
|
94
111
|
end
|
|
95
112
|
|
|
96
113
|
def close
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
window
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
185
|
-
buffer = pool.create_buffer(0, width, height, width * 4, :
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
266
|
+
file.unlink
|
|
197
267
|
file
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|