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
@@ -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, _screen_num = parse_display_name(display_name)
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
- format_byte, header, image_data = request_encoder.put_image_data(
94
- format: format,
95
- drawable: drawable,
96
- gc: gc,
97
- width: width,
98
- height: height,
99
- dst_x: dst_x,
100
- dst_y: dst_y,
101
- depth: depth,
102
- data: data
103
- )
104
- send_request_with_data(72, format_byte, header, image_data)
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&.empty?
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
- init_request = [
169
- 0x6C,
170
- 0,
171
- 11, 0,
172
- 0, 0,
173
- 0, 0
174
- ].pack("CCvvvvvv")
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
- @socket.write(init_request)
177
- @socket.flush
216
+ @transport.write(init_request)
217
+ @transport.flush
178
218
 
179
- header = @socket.read(8)
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
- raise GUI::BackendUnavailable, "X11 connection failed" unless status == 1
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 = @socket.read(additional_length * 4)
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
- @resource_id_base = data[4, 4].unpack1("V")
194
- @resource_id_mask = data[8, 4].unpack1("V")
195
-
196
- vendor_length = data[16, 2].unpack1("v")
197
- num_screens = data[20, 1].unpack1("C")
198
- num_formats = data[21, 1].unpack1("C")
199
-
200
- offset = 32 + pad_length(vendor_length) + num_formats * 8
201
-
202
- if num_screens > 0
203
- @root = data[offset, 4].unpack1("V")
204
- @root_depth = data[offset + 38, 1].unpack1("C")
205
- @root_visual = data[offset + 32, 4].unpack1("V")
206
- @white_pixel = data[offset + 8, 4].unpack1("V")
207
- @black_pixel = data[offset + 12, 4].unpack1("V")
208
- end
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 read_reply
220
- header = transport.read(32)
221
- return nil unless header && header.bytesize == 32
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
- additional = header[4, 4].unpack1("V")
224
- if additional > 0
225
- header + transport.read(additional * 4)
226
- else
227
- header
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 read_event
232
- event_parser.parse(transport.read(32))
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
- def resolve_atom(name)
236
- case name
237
- when :wm_name then 39
238
- when :string then 31
239
- when :atom then 4
240
- when :wm_protocols then intern_atom("WM_PROTOCOLS")
241
- when :wm_delete_window then intern_atom("WM_DELETE_WINDOW")
242
- else
243
- name.is_a?(Integer) ? name : intern_atom(name.to_s)
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 pack_property_data(data, format)
248
- request_encoder.pack_property_data(data, format)
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 pad_to_4(str)
252
- request_encoder.pad_to_4(str)
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 transport
260
- return @transport if @transport&.socket.equal?(@socket)
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("ss")
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("ss")
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