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
|
@@ -4,24 +4,44 @@ require "socket"
|
|
|
4
4
|
require_relative "../backend"
|
|
5
5
|
require_relative "atom_cache"
|
|
6
6
|
require_relative "event_parser"
|
|
7
|
+
require_relative "key_mapper"
|
|
7
8
|
require_relative "request_encoder"
|
|
9
|
+
require_relative "setup_parser"
|
|
8
10
|
require_relative "transport"
|
|
11
|
+
require_relative "x_authority"
|
|
9
12
|
|
|
10
13
|
module RBGL
|
|
11
14
|
module GUI
|
|
12
15
|
module X11
|
|
13
16
|
class Connection
|
|
17
|
+
DEFAULT_REPLY_TIMEOUT = 5.0
|
|
18
|
+
|
|
19
|
+
ATOM_NAMES = {
|
|
20
|
+
wm_protocols: "WM_PROTOCOLS",
|
|
21
|
+
wm_delete_window: "WM_DELETE_WINDOW"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
14
24
|
attr_reader :default_screen, :resource_id_base, :resource_id_mask
|
|
15
25
|
attr_reader :root, :root_depth, :root_visual, :white_pixel, :black_pixel
|
|
26
|
+
attr_reader :maximum_request_length, :bits_per_pixel, :scanline_pad
|
|
27
|
+
attr_reader :visual_class, :red_mask, :green_mask, :blue_mask, :image_byte_order
|
|
16
28
|
|
|
17
|
-
def initialize(display_name)
|
|
18
|
-
host, display_num,
|
|
29
|
+
def initialize(display_name, env: ENV, reply_timeout: DEFAULT_REPLY_TIMEOUT)
|
|
30
|
+
host, display_num, screen_num = parse_display_name(display_name)
|
|
19
31
|
@socket = connect(host, display_num)
|
|
20
32
|
@transport = Transport.new(@socket)
|
|
21
33
|
@resource_id_counter = 0
|
|
22
34
|
@pending_events = []
|
|
35
|
+
@pending_replies = {}
|
|
36
|
+
@sequence = 0
|
|
37
|
+
@default_screen = screen_num
|
|
38
|
+
@reply_timeout = reply_timeout
|
|
23
39
|
|
|
24
|
-
handshake
|
|
40
|
+
handshake(host: host, display_num: display_num, env: env)
|
|
41
|
+
load_keyboard_mapping
|
|
42
|
+
rescue StandardError
|
|
43
|
+
@transport&.close
|
|
44
|
+
raise
|
|
25
45
|
end
|
|
26
46
|
|
|
27
47
|
def generate_id
|
|
@@ -34,7 +54,7 @@ module RBGL
|
|
|
34
54
|
end
|
|
35
55
|
|
|
36
56
|
def flush
|
|
37
|
-
transport.flush
|
|
57
|
+
@transport.flush
|
|
38
58
|
end
|
|
39
59
|
|
|
40
60
|
def atom(name)
|
|
@@ -53,13 +73,20 @@ module RBGL
|
|
|
53
73
|
change_property(window, :wm_protocols, :atom, [wm_delete_window_atom], format: 32)
|
|
54
74
|
end
|
|
55
75
|
|
|
76
|
+
def key_for_keycode(keycode)
|
|
77
|
+
load_keyboard_mapping unless @keyboard_mapping
|
|
78
|
+
keysyms = @keyboard_mapping[keycode]
|
|
79
|
+
keysym = keysyms&.find { |candidate| !candidate.zero? }
|
|
80
|
+
KeyMapper.key(keysym, fallback: keycode)
|
|
81
|
+
end
|
|
82
|
+
|
|
56
83
|
def pending
|
|
57
|
-
transport.pending
|
|
84
|
+
@pending_events.length + @transport.pending
|
|
58
85
|
end
|
|
59
86
|
|
|
60
87
|
def create_window(depth:, wid:, parent:, x:, y:, width:, height:,
|
|
61
88
|
border_width:, window_class:, visual:, value_mask:, values:)
|
|
62
|
-
request = request_encoder.create_window_data(
|
|
89
|
+
depth_byte, request = request_encoder.create_window_data(
|
|
63
90
|
depth: depth,
|
|
64
91
|
wid: wid,
|
|
65
92
|
parent: parent,
|
|
@@ -73,7 +100,7 @@ module RBGL
|
|
|
73
100
|
value_mask: value_mask,
|
|
74
101
|
values: values
|
|
75
102
|
)
|
|
76
|
-
send_request(1, request)
|
|
103
|
+
send_request(1, request, depth_byte)
|
|
77
104
|
end
|
|
78
105
|
|
|
79
106
|
def map_window(wid)
|
|
@@ -89,19 +116,30 @@ module RBGL
|
|
|
89
116
|
send_request(55, request)
|
|
90
117
|
end
|
|
91
118
|
|
|
92
|
-
def put_image(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
data
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
def put_image(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:, bytes_per_line:)
|
|
120
|
+
maximum_data = (@maximum_request_length * 4) - 24
|
|
121
|
+
if bytes_per_line > maximum_data
|
|
122
|
+
raise GUI::BackendUnavailable, "An X11 image row exceeds the server request limit"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
rows_per_request = [maximum_data / bytes_per_line, 1].max
|
|
126
|
+
offset_y = 0
|
|
127
|
+
while offset_y < height
|
|
128
|
+
row_count = [rows_per_request, height - offset_y].min
|
|
129
|
+
chunk = data.byteslice(offset_y * bytes_per_line, row_count * bytes_per_line)
|
|
130
|
+
send_image_chunk(
|
|
131
|
+
format: format,
|
|
132
|
+
drawable: drawable,
|
|
133
|
+
gc: gc,
|
|
134
|
+
width: width,
|
|
135
|
+
height: row_count,
|
|
136
|
+
dst_x: dst_x,
|
|
137
|
+
dst_y: dst_y + offset_y,
|
|
138
|
+
depth: depth,
|
|
139
|
+
data: chunk
|
|
140
|
+
)
|
|
141
|
+
offset_y += row_count
|
|
142
|
+
end
|
|
105
143
|
end
|
|
106
144
|
|
|
107
145
|
def change_property(window, property, type, data, mode: :replace, format: 8)
|
|
@@ -120,12 +158,10 @@ module RBGL
|
|
|
120
158
|
|
|
121
159
|
def intern_atom(name, only_if_exists: false)
|
|
122
160
|
request = request_encoder.intern_atom_data(name)
|
|
123
|
-
send_request(16, request, only_if_exists ? 1 : 0)
|
|
161
|
+
sequence = send_request(16, request, only_if_exists ? 1 : 0)
|
|
124
162
|
flush
|
|
125
163
|
|
|
126
|
-
reply = read_reply
|
|
127
|
-
return 0 unless reply
|
|
128
|
-
|
|
164
|
+
reply = read_reply(sequence)
|
|
129
165
|
reply[8, 4].unpack1("V")
|
|
130
166
|
end
|
|
131
167
|
|
|
@@ -137,18 +173,22 @@ module RBGL
|
|
|
137
173
|
end
|
|
138
174
|
|
|
139
175
|
def read_events
|
|
140
|
-
while pending > 0
|
|
176
|
+
while @transport.pending > 0
|
|
141
177
|
event = read_event
|
|
142
178
|
@pending_events << event if event
|
|
143
179
|
end
|
|
144
180
|
end
|
|
145
181
|
|
|
182
|
+
def close
|
|
183
|
+
@transport.close
|
|
184
|
+
end
|
|
185
|
+
|
|
146
186
|
private
|
|
147
187
|
|
|
148
188
|
def parse_display_name(name)
|
|
149
189
|
if name =~ /^(?:(.*):)?(\d+)(?:\.(\d+))?$/
|
|
150
190
|
host = ::Regexp.last_match(1)
|
|
151
|
-
host = nil if host
|
|
191
|
+
host = nil if host && host.empty?
|
|
152
192
|
[host, ::Regexp.last_match(2).to_i, (::Regexp.last_match(3) || 0).to_i]
|
|
153
193
|
else
|
|
154
194
|
raise GUI::BackendUnavailable, "Invalid X11 display name: #{name}"
|
|
@@ -164,102 +204,160 @@ module RBGL
|
|
|
164
204
|
end
|
|
165
205
|
end
|
|
166
206
|
|
|
167
|
-
def handshake
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
207
|
+
def handshake(host: nil, display_num: 0, env: ENV)
|
|
208
|
+
auth_host = local_display_host?(host) ? nil : host
|
|
209
|
+
cookie = XAuthority.cookie_for(host: auth_host, display_number: display_num, env: env)
|
|
210
|
+
auth_name = cookie ? XAuthority::AUTH_NAME : ""
|
|
211
|
+
auth_data = cookie || ""
|
|
212
|
+
init_request = [0x6C, 0, 11, 0, auth_name.bytesize, auth_data.bytesize, 0].pack("CCvvvvv")
|
|
213
|
+
init_request << request_encoder.pad_to_4(auth_name)
|
|
214
|
+
init_request << request_encoder.pad_to_4(auth_data)
|
|
175
215
|
|
|
176
|
-
@
|
|
177
|
-
@
|
|
216
|
+
@transport.write(init_request)
|
|
217
|
+
@transport.flush
|
|
178
218
|
|
|
179
|
-
header = @
|
|
219
|
+
header = @transport.read_exact(8, timeout: @reply_timeout)
|
|
180
220
|
raise GUI::BackendUnavailable, "X11 connection closed during handshake" unless header&.bytesize == 8
|
|
181
221
|
|
|
182
222
|
status = header.unpack1("C")
|
|
183
|
-
|
|
184
|
-
|
|
223
|
+
unless status == 1
|
|
224
|
+
reason_length = header.getbyte(1)
|
|
225
|
+
reason = if reason_length.positive?
|
|
226
|
+
@transport.read_exact(pad_length(reason_length), timeout: @reply_timeout)
|
|
227
|
+
.byteslice(0, reason_length)
|
|
228
|
+
end
|
|
229
|
+
detail = reason && !reason.empty? ? ": #{reason}" : ""
|
|
230
|
+
raise GUI::BackendUnavailable, "X11 connection failed#{detail}"
|
|
231
|
+
end
|
|
185
232
|
|
|
186
233
|
additional_length = header[6, 2].unpack1("v")
|
|
187
|
-
data = @
|
|
234
|
+
data = @transport.read_exact(additional_length * 4, timeout: @reply_timeout)
|
|
188
235
|
|
|
189
236
|
parse_server_info(data)
|
|
237
|
+
rescue EOFError, IO::WaitReadable => error
|
|
238
|
+
raise GUI::BackendUnavailable, "X11 handshake failed: #{error.message}"
|
|
190
239
|
end
|
|
191
240
|
|
|
192
241
|
def parse_server_info(data)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
setup = SetupParser.new.parse(data)
|
|
243
|
+
screen = setup.screens.fetch(@default_screen)
|
|
244
|
+
format = setup.pixmap_formats.find { |candidate| candidate.depth == screen.root_depth }
|
|
245
|
+
depth = screen.depths.find { |candidate| candidate.depth == screen.root_depth }
|
|
246
|
+
visual = depth&.visuals&.find { |candidate| candidate.id == screen.root_visual }
|
|
247
|
+
raise GUI::BackendUnavailable, "No X11 pixmap format for root depth #{screen.root_depth}" unless format
|
|
248
|
+
raise GUI::BackendUnavailable, "No X11 visual group for root depth #{screen.root_depth}" unless depth
|
|
249
|
+
raise GUI::BackendUnavailable, "No X11 root visual metadata for #{screen.root_visual}" unless visual
|
|
250
|
+
|
|
251
|
+
@resource_id_base = setup.resource_id_base
|
|
252
|
+
@resource_id_mask = setup.resource_id_mask
|
|
253
|
+
@maximum_request_length = setup.maximum_request_length
|
|
254
|
+
@minimum_keycode = setup.minimum_keycode
|
|
255
|
+
@maximum_keycode = setup.maximum_keycode
|
|
256
|
+
@image_byte_order = setup.image_byte_order
|
|
257
|
+
@root = screen.root
|
|
258
|
+
@root_depth = screen.root_depth
|
|
259
|
+
@root_visual = screen.root_visual
|
|
260
|
+
@white_pixel = screen.white_pixel
|
|
261
|
+
@black_pixel = screen.black_pixel
|
|
262
|
+
@bits_per_pixel = format.bits_per_pixel
|
|
263
|
+
@scanline_pad = format.scanline_pad
|
|
264
|
+
@visual_class = visual.visual_class
|
|
265
|
+
@red_mask = visual.red_mask
|
|
266
|
+
@green_mask = visual.green_mask
|
|
267
|
+
@blue_mask = visual.blue_mask
|
|
268
|
+
rescue IndexError
|
|
269
|
+
raise GUI::BackendUnavailable, "X11 screen #{@default_screen} is unavailable"
|
|
209
270
|
end
|
|
210
271
|
|
|
211
272
|
def send_request(opcode, data, extra = 0)
|
|
212
|
-
transport.write(request_encoder.request_packet(opcode, data, extra))
|
|
273
|
+
@transport.write(request_encoder.request_packet(opcode, data, extra))
|
|
274
|
+
next_sequence
|
|
213
275
|
end
|
|
214
276
|
|
|
215
277
|
def send_request_with_data(opcode, extra, header_data, bulk_data)
|
|
216
|
-
transport.write(request_encoder.request_packet_with_data(opcode, extra, header_data, bulk_data))
|
|
278
|
+
@transport.write(request_encoder.request_packet_with_data(opcode, extra, header_data, bulk_data))
|
|
279
|
+
next_sequence
|
|
217
280
|
end
|
|
218
281
|
|
|
219
|
-
def
|
|
220
|
-
header =
|
|
221
|
-
|
|
282
|
+
def send_image_chunk(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:)
|
|
283
|
+
format_byte, header, image_data = request_encoder.put_image_data(
|
|
284
|
+
format: format,
|
|
285
|
+
drawable: drawable,
|
|
286
|
+
gc: gc,
|
|
287
|
+
width: width,
|
|
288
|
+
height: height,
|
|
289
|
+
dst_x: dst_x,
|
|
290
|
+
dst_y: dst_y,
|
|
291
|
+
depth: depth,
|
|
292
|
+
data: data
|
|
293
|
+
)
|
|
294
|
+
send_request_with_data(72, format_byte, header, image_data)
|
|
295
|
+
end
|
|
222
296
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
297
|
+
def load_keyboard_mapping
|
|
298
|
+
count = @maximum_keycode - @minimum_keycode + 1
|
|
299
|
+
request = [@minimum_keycode, count, 0].pack("CCv")
|
|
300
|
+
sequence = send_request(101, request)
|
|
301
|
+
flush
|
|
302
|
+
reply = read_reply(sequence)
|
|
303
|
+
keysyms_per_keycode = reply.getbyte(1)
|
|
304
|
+
keysyms = reply.byteslice(32..).unpack("V*")
|
|
305
|
+
@keyboard_mapping = {}
|
|
306
|
+
|
|
307
|
+
count.times do |index|
|
|
308
|
+
offset = index * keysyms_per_keycode
|
|
309
|
+
@keyboard_mapping[@minimum_keycode + index] = keysyms.slice(offset, keysyms_per_keycode)
|
|
228
310
|
end
|
|
229
311
|
end
|
|
230
312
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
end
|
|
313
|
+
def read_reply(expected_sequence = nil)
|
|
314
|
+
return @pending_replies.delete(expected_sequence) if expected_sequence && @pending_replies.key?(expected_sequence)
|
|
234
315
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
loop do
|
|
317
|
+
packet = read_packet
|
|
318
|
+
type = packet.getbyte(0)
|
|
319
|
+
sequence = packet.byteslice(2, 2).unpack1("v")
|
|
320
|
+
|
|
321
|
+
case type
|
|
322
|
+
when 0
|
|
323
|
+
raise_protocol_error(packet)
|
|
324
|
+
when 1
|
|
325
|
+
return packet if expected_sequence.nil? || sequence == expected_sequence
|
|
326
|
+
|
|
327
|
+
@pending_replies[sequence] = packet
|
|
328
|
+
else
|
|
329
|
+
event = parse_event(packet)
|
|
330
|
+
@pending_events << event if event
|
|
331
|
+
end
|
|
244
332
|
end
|
|
245
333
|
end
|
|
246
334
|
|
|
247
|
-
def
|
|
248
|
-
|
|
335
|
+
def read_event
|
|
336
|
+
packet = read_packet
|
|
337
|
+
type = packet.getbyte(0)
|
|
338
|
+
return raise_protocol_error(packet) if type.zero?
|
|
339
|
+
|
|
340
|
+
if type == 1
|
|
341
|
+
sequence = packet.byteslice(2, 2).unpack1("v")
|
|
342
|
+
@pending_replies[sequence] = packet
|
|
343
|
+
return nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
parse_event(packet)
|
|
249
347
|
end
|
|
250
348
|
|
|
251
|
-
def
|
|
252
|
-
|
|
349
|
+
def resolve_atom(name)
|
|
350
|
+
return name if name.is_a?(Integer)
|
|
351
|
+
|
|
352
|
+
intern_atom(ATOM_NAMES.fetch(name, name.to_s))
|
|
253
353
|
end
|
|
254
354
|
|
|
255
355
|
def pad_length(len)
|
|
256
356
|
((len + 3) / 4) * 4
|
|
257
357
|
end
|
|
258
358
|
|
|
259
|
-
def
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
@transport = Transport.new(@socket)
|
|
359
|
+
def local_display_host?(host)
|
|
360
|
+
host.nil? || host.empty? || host == "unix"
|
|
263
361
|
end
|
|
264
362
|
|
|
265
363
|
def request_encoder
|
|
@@ -273,6 +371,38 @@ module RBGL
|
|
|
273
371
|
def atom_cache
|
|
274
372
|
@atom_cache ||= AtomCache.new { |name| resolve_atom(name) }
|
|
275
373
|
end
|
|
374
|
+
|
|
375
|
+
def read_packet
|
|
376
|
+
header = @transport.read_exact(32, timeout: @reply_timeout)
|
|
377
|
+
return header unless header.getbyte(0) == 1
|
|
378
|
+
|
|
379
|
+
additional = header.byteslice(4, 4).unpack1("V")
|
|
380
|
+
additional.positive? ? header + @transport.read_exact(additional * 4, timeout: @reply_timeout) : header
|
|
381
|
+
rescue EOFError, IO::WaitReadable => error
|
|
382
|
+
raise GUI::BackendUnavailable, error.message
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def parse_event(packet)
|
|
386
|
+
event = event_parser.parse(packet)
|
|
387
|
+
@keyboard_mapping = nil if event&.[](:type) == :mapping_notify && event[:request] == :keyboard
|
|
388
|
+
event
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def raise_protocol_error(packet)
|
|
392
|
+
error_code = packet.getbyte(1)
|
|
393
|
+
sequence = packet.byteslice(2, 2).unpack1("v")
|
|
394
|
+
bad_value = packet.byteslice(4, 4).unpack1("V")
|
|
395
|
+
minor_opcode = packet.byteslice(8, 2).unpack1("v")
|
|
396
|
+
major_opcode = packet.getbyte(10)
|
|
397
|
+
raise GUI::BackendUnavailable,
|
|
398
|
+
"X11 protocol error #{error_code} for request #{sequence} " \
|
|
399
|
+
"(opcode #{major_opcode}.#{minor_opcode}, value #{bad_value})"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def next_sequence
|
|
403
|
+
@sequence = (@sequence + 1) & 0xFFFF
|
|
404
|
+
end
|
|
405
|
+
|
|
276
406
|
end
|
|
277
407
|
end
|
|
278
408
|
end
|
|
@@ -19,7 +19,7 @@ module RBGL
|
|
|
19
19
|
when 5
|
|
20
20
|
button_event(:button_release, data)
|
|
21
21
|
when 6
|
|
22
|
-
x, y = data[24, 4].unpack("
|
|
22
|
+
x, y = data[24, 4].unpack("s<s<")
|
|
23
23
|
{ type: :motion_notify, x: x, y: y }
|
|
24
24
|
when 12
|
|
25
25
|
{ type: :exposure }
|
|
@@ -34,6 +34,13 @@ module RBGL
|
|
|
34
34
|
message_type: data[8, 4].unpack1("V"),
|
|
35
35
|
data32: data[12, 20].unpack("V5")
|
|
36
36
|
}
|
|
37
|
+
when 34
|
|
38
|
+
{
|
|
39
|
+
type: :mapping_notify,
|
|
40
|
+
request: mapping_request(data.getbyte(1)),
|
|
41
|
+
first_keycode: data.getbyte(4),
|
|
42
|
+
count: data.getbyte(5)
|
|
43
|
+
}
|
|
37
44
|
else
|
|
38
45
|
{ type: :unknown, code: event_type }
|
|
39
46
|
end
|
|
@@ -46,7 +53,7 @@ module RBGL
|
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
def button_event(type, data)
|
|
49
|
-
x, y = data[24, 4].unpack("
|
|
56
|
+
x, y = data[24, 4].unpack("s<s<")
|
|
50
57
|
{
|
|
51
58
|
type: type,
|
|
52
59
|
x: x,
|
|
@@ -54,6 +61,10 @@ module RBGL
|
|
|
54
61
|
button: data[1, 1].unpack1("C")
|
|
55
62
|
}
|
|
56
63
|
end
|
|
64
|
+
|
|
65
|
+
def mapping_request(code)
|
|
66
|
+
{ 0 => :modifier, 1 => :keyboard, 2 => :pointer }.fetch(code, :unknown)
|
|
67
|
+
end
|
|
57
68
|
end
|
|
58
69
|
end
|
|
59
70
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBGL
|
|
4
|
+
module GUI
|
|
5
|
+
module X11
|
|
6
|
+
module KeyMapper
|
|
7
|
+
SPECIAL_KEYSYMS = {
|
|
8
|
+
0xFF08 => :backspace,
|
|
9
|
+
0xFF09 => :tab,
|
|
10
|
+
0xFF0D => :enter,
|
|
11
|
+
0xFF1B => :escape,
|
|
12
|
+
0xFF50 => :home,
|
|
13
|
+
0xFF51 => :left,
|
|
14
|
+
0xFF52 => :up,
|
|
15
|
+
0xFF53 => :right,
|
|
16
|
+
0xFF54 => :down,
|
|
17
|
+
0xFF55 => :page_up,
|
|
18
|
+
0xFF56 => :page_down,
|
|
19
|
+
0xFF57 => :end,
|
|
20
|
+
0xFF63 => :insert,
|
|
21
|
+
0xFFFF => :delete,
|
|
22
|
+
0xFFE1 => :left_shift,
|
|
23
|
+
0xFFE2 => :right_shift,
|
|
24
|
+
0xFFE3 => :left_control,
|
|
25
|
+
0xFFE4 => :right_control,
|
|
26
|
+
0xFFE7 => :left_meta,
|
|
27
|
+
0xFFE8 => :right_meta,
|
|
28
|
+
0xFFE9 => :left_alt,
|
|
29
|
+
0xFFEA => :right_alt
|
|
30
|
+
}.merge((0xFFBE..0xFFC9).zip((1..12).map { |number| :"f#{number}" }).to_h).freeze
|
|
31
|
+
|
|
32
|
+
DIGIT_NAMES = %i[zero one two three four five six seven eight nine].freeze
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def key(keysym, fallback:)
|
|
37
|
+
return fallback if keysym.nil? || keysym.zero?
|
|
38
|
+
return SPECIAL_KEYSYMS[keysym] if SPECIAL_KEYSYMS.key?(keysym)
|
|
39
|
+
return :space if keysym == 0x20
|
|
40
|
+
return DIGIT_NAMES[keysym - 0x30] if keysym.between?(0x30, 0x39)
|
|
41
|
+
return keysym.chr.downcase.to_sym if keysym.between?(0x41, 0x5A) || keysym.between?(0x61, 0x7A)
|
|
42
|
+
|
|
43
|
+
fallback
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBGL
|
|
4
|
+
module GUI
|
|
5
|
+
module X11
|
|
6
|
+
class PixelEncoder
|
|
7
|
+
TRUE_COLOR_CLASSES = [4, 5].freeze
|
|
8
|
+
IMAGE_BYTE_ORDERS = %i[little big].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :bytes_per_pixel
|
|
11
|
+
|
|
12
|
+
def initialize(bits_per_pixel:, scanline_pad:, visual_class:, red_mask:, green_mask:, blue_mask:,
|
|
13
|
+
image_byte_order: :little)
|
|
14
|
+
unless TRUE_COLOR_CLASSES.include?(visual_class)
|
|
15
|
+
raise GUI::BackendUnavailable, "X11 root visual must be TrueColor or DirectColor"
|
|
16
|
+
end
|
|
17
|
+
unless [16, 24, 32].include?(bits_per_pixel)
|
|
18
|
+
raise GUI::BackendUnavailable, "Unsupported X11 pixel width: #{bits_per_pixel}"
|
|
19
|
+
end
|
|
20
|
+
unless IMAGE_BYTE_ORDERS.include?(image_byte_order)
|
|
21
|
+
raise GUI::BackendUnavailable, "Unsupported X11 image byte order: #{image_byte_order}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@bytes_per_pixel = bits_per_pixel / 8
|
|
25
|
+
@scanline_alignment = scanline_pad / 8
|
|
26
|
+
@image_byte_order = image_byte_order
|
|
27
|
+
@red_lut = channel_lut(red_mask)
|
|
28
|
+
@green_lut = channel_lut(green_mask)
|
|
29
|
+
@blue_lut = channel_lut(blue_mask)
|
|
30
|
+
@standard_bgra = bits_per_pixel == 32 && image_byte_order == :little &&
|
|
31
|
+
red_mask == 0x00FF0000 && green_mask == 0x0000FF00 && blue_mask == 0x000000FF
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def bytes_per_line(width)
|
|
35
|
+
aligned_length(width * @bytes_per_pixel)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def encode(framebuffer)
|
|
39
|
+
stride = bytes_per_line(framebuffer.width)
|
|
40
|
+
if @standard_bgra
|
|
41
|
+
return pad_rows(framebuffer.to_bgra_bytes, framebuffer.width * 4, stride, framebuffer.height)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
output = String.new(capacity: stride * framebuffer.height, encoding: Encoding::BINARY)
|
|
45
|
+
row_pixels = 0
|
|
46
|
+
|
|
47
|
+
framebuffer.each_packed_pixel do |color|
|
|
48
|
+
append_pixel(
|
|
49
|
+
output,
|
|
50
|
+
@red_lut[(color >> 16) & 0xFF] |
|
|
51
|
+
@green_lut[(color >> 8) & 0xFF] |
|
|
52
|
+
@blue_lut[color & 0xFF]
|
|
53
|
+
)
|
|
54
|
+
row_pixels += 1
|
|
55
|
+
next unless row_pixels == framebuffer.width
|
|
56
|
+
|
|
57
|
+
append_padding(output, stride - (framebuffer.width * @bytes_per_pixel))
|
|
58
|
+
row_pixels = 0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
output
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def encode_rgba(buffer, width, height)
|
|
65
|
+
if @standard_bgra
|
|
66
|
+
output = String.new(capacity: bytes_per_line(width) * height, encoding: Encoding::BINARY)
|
|
67
|
+
encode_standard_rgba(output, buffer, width, height)
|
|
68
|
+
return output
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
encode_arbitrary_rgba(buffer, width, height)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def encode_standard_rgba(output, buffer, width, height)
|
|
77
|
+
stride = bytes_per_line(width)
|
|
78
|
+
row_bytes = width * 4
|
|
79
|
+
byte_index = 0
|
|
80
|
+
|
|
81
|
+
height.times do
|
|
82
|
+
width.times do
|
|
83
|
+
output << buffer.getbyte(byte_index + 2) << buffer.getbyte(byte_index + 1) <<
|
|
84
|
+
buffer.getbyte(byte_index) << buffer.getbyte(byte_index + 3)
|
|
85
|
+
byte_index += 4
|
|
86
|
+
end
|
|
87
|
+
append_padding(output, stride - row_bytes)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def encode_arbitrary_rgba(buffer, width, height)
|
|
92
|
+
stride = bytes_per_line(width)
|
|
93
|
+
output = String.new(capacity: stride * height, encoding: Encoding::BINARY)
|
|
94
|
+
byte_index = 0
|
|
95
|
+
|
|
96
|
+
height.times do
|
|
97
|
+
width.times do
|
|
98
|
+
append_pixel(
|
|
99
|
+
output,
|
|
100
|
+
@red_lut[buffer.getbyte(byte_index)] |
|
|
101
|
+
@green_lut[buffer.getbyte(byte_index + 1)] |
|
|
102
|
+
@blue_lut[buffer.getbyte(byte_index + 2)]
|
|
103
|
+
)
|
|
104
|
+
byte_index += 4
|
|
105
|
+
end
|
|
106
|
+
append_padding(output, stride - (width * @bytes_per_pixel))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
output
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def append_pixel(output, pixel)
|
|
113
|
+
if @image_byte_order == :little
|
|
114
|
+
index = 0
|
|
115
|
+
while index < @bytes_per_pixel
|
|
116
|
+
output << ((pixel >> (index * 8)) & 0xFF)
|
|
117
|
+
index += 1
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
index = @bytes_per_pixel - 1
|
|
121
|
+
while index >= 0
|
|
122
|
+
output << ((pixel >> (index * 8)) & 0xFF)
|
|
123
|
+
index -= 1
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def append_padding(output, length)
|
|
129
|
+
output << ("\x00" * length) if length.positive?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def channel_lut(mask)
|
|
133
|
+
raise GUI::BackendUnavailable, "X11 visual has an empty color mask" if mask.zero?
|
|
134
|
+
|
|
135
|
+
shift = 0
|
|
136
|
+
shifted = mask
|
|
137
|
+
while shifted & 1 == 0
|
|
138
|
+
shifted >>= 1
|
|
139
|
+
shift += 1
|
|
140
|
+
end
|
|
141
|
+
maximum = shifted
|
|
142
|
+
Array.new(256) { |value| ((value * maximum / 255.0).round << shift) }.freeze
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def pad_rows(bytes, row_bytes, stride, height)
|
|
146
|
+
return bytes if row_bytes == stride
|
|
147
|
+
|
|
148
|
+
output = String.new(capacity: stride * height, encoding: Encoding::BINARY)
|
|
149
|
+
height.times do |row|
|
|
150
|
+
output << bytes.byteslice(row * row_bytes, row_bytes)
|
|
151
|
+
append_padding(output, stride - row_bytes)
|
|
152
|
+
end
|
|
153
|
+
output
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def aligned_length(length)
|
|
157
|
+
return length if @scanline_alignment <= 1
|
|
158
|
+
|
|
159
|
+
((length + @scanline_alignment - 1) / @scanline_alignment) * @scanline_alignment
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|