rbgl 0.1.0 → 1.0.1
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +20 -2
- 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 +150 -40
- data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
- data/lib/rbgl/engine/context.rb +133 -63
- data/lib/rbgl/engine/framebuffer.rb +153 -48
- data/lib/rbgl/engine/immutable_color.rb +32 -0
- data/lib/rbgl/engine/pipeline.rb +38 -7
- data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
- data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
- data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
- data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
- data/lib/rbgl/engine/rasterizer.rb +65 -171
- data/lib/rbgl/engine/shader/base_shader.rb +51 -0
- data/lib/rbgl/engine/shader/builtins.rb +257 -0
- data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
- data/lib/rbgl/engine/shader.rb +5 -318
- data/lib/rbgl/engine/texture.rb +264 -51
- data/lib/rbgl/engine.rb +4 -1
- data/lib/rbgl/gui/backend.rb +21 -34
- data/lib/rbgl/gui/backend_factory.rb +89 -0
- data/lib/rbgl/gui/cocoa/backend.rb +59 -46
- 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 +63 -0
- data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
- data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
- data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
- data/lib/rbgl/gui/file_backend.rb +50 -55
- data/lib/rbgl/gui/wayland/backend.rb +214 -60
- data/lib/rbgl/gui/wayland/codec.rb +50 -0
- data/lib/rbgl/gui/wayland/connection.rb +188 -231
- data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
- data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
- data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
- data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
- data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
- data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
- data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
- data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
- data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
- data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
- data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
- data/lib/rbgl/gui/window/render_loop.rb +111 -0
- data/lib/rbgl/gui/window.rb +74 -84
- data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
- data/lib/rbgl/gui/x11/backend.rb +73 -49
- data/lib/rbgl/gui/x11/connection.rb +284 -219
- data/lib/rbgl/gui/x11/event_parser.rb +71 -0
- 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 +155 -0
- data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
- data/lib/rbgl/gui/x11/transport.rb +81 -0
- data/lib/rbgl/gui/x11/x_authority.rb +107 -0
- data/lib/rbgl/gui.rb +3 -5
- data/lib/rbgl/version.rb +1 -1
- metadata +63 -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
|
@@ -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
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBGL
|
|
4
|
+
module GUI
|
|
5
|
+
module X11
|
|
6
|
+
class RequestEncoder
|
|
7
|
+
EVENT_MASKS = {
|
|
8
|
+
exposure: 0x8000,
|
|
9
|
+
key_press: 0x0001,
|
|
10
|
+
key_release: 0x0002,
|
|
11
|
+
button_press: 0x0004,
|
|
12
|
+
button_release: 0x0008,
|
|
13
|
+
pointer_motion: 0x0040,
|
|
14
|
+
structure_notify: 0x020000
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
WINDOW_CLASSES = {
|
|
18
|
+
input_output: 1,
|
|
19
|
+
input_only: 2
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
PROPERTY_MODES = {
|
|
23
|
+
replace: 0,
|
|
24
|
+
prepend: 1,
|
|
25
|
+
append: 2
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
IMAGE_FORMATS = {
|
|
29
|
+
bitmap: 0,
|
|
30
|
+
xy_pixmap: 1,
|
|
31
|
+
z_pixmap: 2
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
def create_window_data(depth:, wid:, parent:, x:, y:, width:, height:,
|
|
35
|
+
border_width:, window_class:, visual:, value_mask:, values:)
|
|
36
|
+
mask = 0
|
|
37
|
+
value_list = []
|
|
38
|
+
|
|
39
|
+
if value_mask.include?(:back_pixel)
|
|
40
|
+
mask |= 0x0002
|
|
41
|
+
value_list << values[:back_pixel]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if value_mask.include?(:event_mask)
|
|
45
|
+
mask |= 0x0800
|
|
46
|
+
value_list << encode_event_mask(values[:event_mask])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
data = [
|
|
50
|
+
wid,
|
|
51
|
+
parent,
|
|
52
|
+
x, y,
|
|
53
|
+
width, height,
|
|
54
|
+
border_width,
|
|
55
|
+
WINDOW_CLASSES.fetch(window_class),
|
|
56
|
+
visual,
|
|
57
|
+
mask
|
|
58
|
+
].pack("VVs<s<vvvvVV") + value_list.pack("V*")
|
|
59
|
+
|
|
60
|
+
[depth, data]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_gc_data(gc_id, drawable, values = {})
|
|
64
|
+
mask = 0
|
|
65
|
+
value_list = []
|
|
66
|
+
|
|
67
|
+
if values[:foreground]
|
|
68
|
+
mask |= 0x0004
|
|
69
|
+
value_list << values[:foreground]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if values[:background]
|
|
73
|
+
mask |= 0x0008
|
|
74
|
+
value_list << values[:background]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
[gc_id, drawable, mask].pack("VVV") + value_list.pack("V*")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def put_image_data(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:)
|
|
81
|
+
format_byte = IMAGE_FORMATS.fetch(format)
|
|
82
|
+
header = [
|
|
83
|
+
drawable,
|
|
84
|
+
gc,
|
|
85
|
+
width, height,
|
|
86
|
+
dst_x, dst_y,
|
|
87
|
+
0,
|
|
88
|
+
depth
|
|
89
|
+
].pack("VVvvs<s<CC") + "\x00\x00"
|
|
90
|
+
|
|
91
|
+
[format_byte, header, data]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def change_property_data(window, property_atom, type_atom, data, mode: :replace, format: 8)
|
|
95
|
+
data_bytes, value_count = pack_property_data(data, format)
|
|
96
|
+
request = [window, property_atom, type_atom, format].pack("VVVC") +
|
|
97
|
+
"\x00\x00\x00" +
|
|
98
|
+
[value_count].pack("V") +
|
|
99
|
+
pad_to_4(data_bytes)
|
|
100
|
+
|
|
101
|
+
[PROPERTY_MODES.fetch(mode), request]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def intern_atom_data(name)
|
|
105
|
+
[name.bytesize, 0].pack("vv") + pad_to_4(name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def request_packet(opcode, data, extra = 0)
|
|
109
|
+
encode_packet(opcode, extra, data)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def request_packet_with_data(opcode, extra, header_data, bulk_data)
|
|
113
|
+
encode_packet(opcode, extra, header_data + bulk_data)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def pack_property_data(data, format)
|
|
117
|
+
case format
|
|
118
|
+
when 8
|
|
119
|
+
bytes = data.to_s.b
|
|
120
|
+
[bytes, bytes.bytesize]
|
|
121
|
+
when 16
|
|
122
|
+
values = Array(data)
|
|
123
|
+
[values.pack("v*"), values.size]
|
|
124
|
+
when 32
|
|
125
|
+
values = Array(data)
|
|
126
|
+
[values.pack("V*"), values.size]
|
|
127
|
+
else
|
|
128
|
+
raise ArgumentError, "Unsupported property format: #{format}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def pad_to_4(str)
|
|
133
|
+
bytes = str.b
|
|
134
|
+
padding = (4 - (bytes.bytesize % 4)) % 4
|
|
135
|
+
bytes + ("\x00" * padding)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def encode_packet(opcode, extra, data)
|
|
141
|
+
length = (4 + data.bytesize + 3) / 4
|
|
142
|
+
raise ArgumentError, "X11 request exceeds the 16-bit core protocol limit" if length > 65_535
|
|
143
|
+
|
|
144
|
+
header = [opcode, extra, length].pack("CCv")
|
|
145
|
+
padding = "\x00" * ((length * 4) - 4 - data.bytesize)
|
|
146
|
+
header + data + padding
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def encode_event_mask(events)
|
|
150
|
+
Array(events).sum { |event| EVENT_MASKS.fetch(event) }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBGL
|
|
4
|
+
module GUI
|
|
5
|
+
module X11
|
|
6
|
+
class SetupParser
|
|
7
|
+
Setup = Struct.new(
|
|
8
|
+
:resource_id_base,
|
|
9
|
+
:resource_id_mask,
|
|
10
|
+
:maximum_request_length,
|
|
11
|
+
:image_byte_order,
|
|
12
|
+
:minimum_keycode,
|
|
13
|
+
:maximum_keycode,
|
|
14
|
+
:pixmap_formats,
|
|
15
|
+
:screens,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
PixmapFormat = Struct.new(:depth, :bits_per_pixel, :scanline_pad, keyword_init: true)
|
|
19
|
+
Depth = Struct.new(:depth, :visuals, keyword_init: true)
|
|
20
|
+
Screen = Struct.new(
|
|
21
|
+
:root,
|
|
22
|
+
:white_pixel,
|
|
23
|
+
:black_pixel,
|
|
24
|
+
:root_visual,
|
|
25
|
+
:root_depth,
|
|
26
|
+
:depths,
|
|
27
|
+
keyword_init: true
|
|
28
|
+
) do
|
|
29
|
+
def visuals
|
|
30
|
+
depths.flat_map(&:visuals)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
Visual = Struct.new(:id, :visual_class, :red_mask, :green_mask, :blue_mask, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
def parse(data)
|
|
36
|
+
raise ArgumentError, "Truncated X11 setup response" if data.bytesize < 32
|
|
37
|
+
|
|
38
|
+
vendor_length = data.byteslice(16, 2).unpack1("v")
|
|
39
|
+
screen_count = data.getbyte(20)
|
|
40
|
+
format_count = data.getbyte(21)
|
|
41
|
+
format_offset = 32 + padded_length(vendor_length)
|
|
42
|
+
formats = parse_formats(data, format_offset, format_count)
|
|
43
|
+
screens = parse_screens(data, format_offset + (format_count * 8), screen_count)
|
|
44
|
+
|
|
45
|
+
Setup.new(
|
|
46
|
+
resource_id_base: data.byteslice(4, 4).unpack1("V"),
|
|
47
|
+
resource_id_mask: data.byteslice(8, 4).unpack1("V"),
|
|
48
|
+
maximum_request_length: data.byteslice(18, 2).unpack1("v"),
|
|
49
|
+
image_byte_order: parse_image_byte_order(data.getbyte(22)),
|
|
50
|
+
minimum_keycode: data.getbyte(26),
|
|
51
|
+
maximum_keycode: data.getbyte(27),
|
|
52
|
+
pixmap_formats: formats,
|
|
53
|
+
screens: screens
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def parse_formats(data, offset, count)
|
|
60
|
+
count.times.map do |index|
|
|
61
|
+
bytes = required_slice(data, offset + (index * 8), 8)
|
|
62
|
+
PixmapFormat.new(
|
|
63
|
+
depth: bytes.getbyte(0),
|
|
64
|
+
bits_per_pixel: bytes.getbyte(1),
|
|
65
|
+
scanline_pad: bytes.getbyte(2)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_screens(data, offset, count)
|
|
71
|
+
screens = []
|
|
72
|
+
count.times do
|
|
73
|
+
screen, offset = parse_screen(data, offset)
|
|
74
|
+
screens << screen
|
|
75
|
+
end
|
|
76
|
+
screens
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_screen(data, offset)
|
|
80
|
+
header = required_slice(data, offset, 40)
|
|
81
|
+
depth_count = header.getbyte(39)
|
|
82
|
+
cursor = offset + 40
|
|
83
|
+
depths = []
|
|
84
|
+
|
|
85
|
+
depth_count.times do
|
|
86
|
+
depth_header = required_slice(data, cursor, 8)
|
|
87
|
+
depth_value = depth_header.getbyte(0)
|
|
88
|
+
visual_count = depth_header.byteslice(2, 2).unpack1("v")
|
|
89
|
+
cursor += 8
|
|
90
|
+
visuals = []
|
|
91
|
+
visual_count.times do
|
|
92
|
+
visuals << parse_visual(required_slice(data, cursor, 24))
|
|
93
|
+
cursor += 24
|
|
94
|
+
end
|
|
95
|
+
depths << Depth.new(depth: depth_value, visuals: visuals)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
screen = Screen.new(
|
|
99
|
+
root: header.byteslice(0, 4).unpack1("V"),
|
|
100
|
+
white_pixel: header.byteslice(8, 4).unpack1("V"),
|
|
101
|
+
black_pixel: header.byteslice(12, 4).unpack1("V"),
|
|
102
|
+
root_visual: header.byteslice(32, 4).unpack1("V"),
|
|
103
|
+
root_depth: header.getbyte(38),
|
|
104
|
+
depths: depths
|
|
105
|
+
)
|
|
106
|
+
[screen, cursor]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_visual(bytes)
|
|
110
|
+
Visual.new(
|
|
111
|
+
id: bytes.byteslice(0, 4).unpack1("V"),
|
|
112
|
+
visual_class: bytes.getbyte(4),
|
|
113
|
+
red_mask: bytes.byteslice(8, 4).unpack1("V"),
|
|
114
|
+
green_mask: bytes.byteslice(12, 4).unpack1("V"),
|
|
115
|
+
blue_mask: bytes.byteslice(16, 4).unpack1("V")
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def required_slice(data, offset, length)
|
|
120
|
+
bytes = data.byteslice(offset, length)
|
|
121
|
+
raise ArgumentError, "Truncated X11 setup response" unless bytes&.bytesize == length
|
|
122
|
+
|
|
123
|
+
bytes
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def padded_length(length)
|
|
127
|
+
(length + 3) & ~3
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_image_byte_order(value)
|
|
131
|
+
{ 0 => :little, 1 => :big }.fetch(value) do
|
|
132
|
+
raise ArgumentError, "Unsupported X11 image byte order: #{value}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/wait"
|
|
4
|
+
|
|
5
|
+
module RBGL
|
|
6
|
+
module GUI
|
|
7
|
+
module X11
|
|
8
|
+
class Transport
|
|
9
|
+
READ_CHUNK_SIZE = 16_384
|
|
10
|
+
|
|
11
|
+
attr_reader :socket
|
|
12
|
+
|
|
13
|
+
def initialize(socket)
|
|
14
|
+
@socket = socket
|
|
15
|
+
@read_buffer = String.new(encoding: Encoding::BINARY)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write(data)
|
|
19
|
+
@socket.write(data)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read(length)
|
|
23
|
+
read_exact(length)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def read_exact(length, timeout: nil)
|
|
27
|
+
fill_buffer(length, timeout: timeout)
|
|
28
|
+
@read_buffer.slice!(0, length)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def flush
|
|
32
|
+
@socket.flush
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def pending
|
|
36
|
+
read_available
|
|
37
|
+
return 0 if @read_buffer.bytesize < 32
|
|
38
|
+
|
|
39
|
+
packet_size = 32
|
|
40
|
+
if @read_buffer.getbyte(0) == 1
|
|
41
|
+
packet_size += @read_buffer.byteslice(4, 4).unpack1("V") * 4
|
|
42
|
+
end
|
|
43
|
+
@read_buffer.bytesize >= packet_size ? 1 : 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close
|
|
47
|
+
@socket.close unless @socket.closed?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def fill_buffer(length, timeout:)
|
|
53
|
+
deadline = timeout && (monotonic_time + timeout)
|
|
54
|
+
|
|
55
|
+
while @read_buffer.bytesize < length
|
|
56
|
+
wait = deadline && [deadline - monotonic_time, 0].max
|
|
57
|
+
raise IO::EAGAINWaitReadable unless @socket.wait_readable(wait)
|
|
58
|
+
|
|
59
|
+
read_available
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_available
|
|
64
|
+
return unless @socket.wait_readable(0)
|
|
65
|
+
|
|
66
|
+
loop do
|
|
67
|
+
chunk = @socket.read_nonblock(READ_CHUNK_SIZE, exception: false)
|
|
68
|
+
break if chunk == :wait_readable
|
|
69
|
+
raise EOFError, "X11 server closed the connection" if chunk.nil?
|
|
70
|
+
|
|
71
|
+
@read_buffer << chunk
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def monotonic_time
|
|
76
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
|
|
6
|
+
module RBGL
|
|
7
|
+
module GUI
|
|
8
|
+
module X11
|
|
9
|
+
class XAuthority
|
|
10
|
+
FAMILY_INTERNET = 0
|
|
11
|
+
FAMILY_INTERNET6 = 6
|
|
12
|
+
FAMILY_LOCAL = 256
|
|
13
|
+
FAMILY_WILD = 65_535
|
|
14
|
+
AUTH_NAME = "MIT-MAGIC-COOKIE-1"
|
|
15
|
+
|
|
16
|
+
Entry = Struct.new(:family, :address, :display_number, :name, :data, keyword_init: true)
|
|
17
|
+
|
|
18
|
+
def self.cookie_for(host:, display_number:, env: ENV)
|
|
19
|
+
path = env["XAUTHORITY"] || default_path(env)
|
|
20
|
+
return nil unless path && File.file?(path)
|
|
21
|
+
|
|
22
|
+
new(File.binread(path)).cookie_for(host: host, display_number: display_number)
|
|
23
|
+
rescue ArgumentError, EOFError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.default_path(env)
|
|
28
|
+
home = env["HOME"] || Dir.home
|
|
29
|
+
File.join(home, ".Xauthority")
|
|
30
|
+
rescue ArgumentError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(data)
|
|
35
|
+
@entries = parse(data)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cookie_for(host:, display_number:)
|
|
39
|
+
candidates = @entries.select do |entry|
|
|
40
|
+
(entry.display_number.empty? || entry.display_number == display_number.to_s) && entry.name == AUTH_NAME
|
|
41
|
+
end
|
|
42
|
+
candidates.find { |entry| address_matches?(entry, host) }&.data
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parse(data)
|
|
48
|
+
offset = 0
|
|
49
|
+
entries = []
|
|
50
|
+
|
|
51
|
+
while offset < data.bytesize
|
|
52
|
+
family, offset = read_u16(data, offset)
|
|
53
|
+
address, offset = read_field(data, offset)
|
|
54
|
+
display_number, offset = read_field(data, offset)
|
|
55
|
+
name, offset = read_field(data, offset)
|
|
56
|
+
cookie, offset = read_field(data, offset)
|
|
57
|
+
entries << Entry.new(
|
|
58
|
+
family: family,
|
|
59
|
+
address: address,
|
|
60
|
+
display_number: display_number,
|
|
61
|
+
name: name,
|
|
62
|
+
data: cookie
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
entries
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_u16(data, offset)
|
|
70
|
+
bytes = data.byteslice(offset, 2)
|
|
71
|
+
raise EOFError, "Truncated Xauthority entry" unless bytes&.bytesize == 2
|
|
72
|
+
|
|
73
|
+
[bytes.unpack1("n"), offset + 2]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def read_field(data, offset)
|
|
77
|
+
length, offset = read_u16(data, offset)
|
|
78
|
+
value = data.byteslice(offset, length)
|
|
79
|
+
raise EOFError, "Truncated Xauthority field" unless value&.bytesize == length
|
|
80
|
+
|
|
81
|
+
[value, offset + length]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def address_matches?(entry, host)
|
|
85
|
+
return true if entry.family == FAMILY_WILD
|
|
86
|
+
return local_address_matches?(entry.address) if host.nil? && entry.family == FAMILY_LOCAL
|
|
87
|
+
return false unless [FAMILY_INTERNET, FAMILY_INTERNET6].include?(entry.family)
|
|
88
|
+
|
|
89
|
+
family = entry.family == FAMILY_INTERNET ? :INET : :INET6
|
|
90
|
+
Addrinfo.getaddrinfo(host, nil, family).any? do |address|
|
|
91
|
+
IPAddr.new(address.ip_address).hton == entry.address
|
|
92
|
+
end
|
|
93
|
+
rescue SocketError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def local_address_matches?(address)
|
|
98
|
+
return true if address.empty?
|
|
99
|
+
|
|
100
|
+
expected = address.downcase
|
|
101
|
+
actual = Socket.gethostname.downcase
|
|
102
|
+
expected == actual || expected.split(".", 2).first == actual.split(".", 2).first
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|