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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +20 -2
  4. data/README.md +130 -74
  5. data/Rakefile +7 -0
  6. data/bench/renderer_bench.rb +283 -0
  7. data/docs/.nojekyll +1 -0
  8. data/docs/index.html +38 -0
  9. data/examples/array_test.rb +1 -1
  10. data/examples/color_test.rb +1 -1
  11. data/examples/cube_spinning.rb +1 -1
  12. data/examples/gradient.rb +1 -1
  13. data/examples/multi_return_test.rb +1 -1
  14. data/examples/plasma.rb +1 -1
  15. data/examples/sphere_raymarch.rb +1 -1
  16. data/examples/triangle_window.rb +1 -1
  17. data/exe/rbgl +42 -0
  18. data/lib/rbgl/cli/doctor.rb +155 -0
  19. data/lib/rbgl/engine/buffer.rb +150 -40
  20. data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
  21. data/lib/rbgl/engine/context.rb +133 -63
  22. data/lib/rbgl/engine/framebuffer.rb +153 -48
  23. data/lib/rbgl/engine/immutable_color.rb +32 -0
  24. data/lib/rbgl/engine/pipeline.rb +38 -7
  25. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
  26. data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
  27. data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
  28. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
  29. data/lib/rbgl/engine/rasterizer.rb +65 -171
  30. data/lib/rbgl/engine/shader/base_shader.rb +51 -0
  31. data/lib/rbgl/engine/shader/builtins.rb +257 -0
  32. data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
  33. data/lib/rbgl/engine/shader.rb +5 -318
  34. data/lib/rbgl/engine/texture.rb +264 -51
  35. data/lib/rbgl/engine.rb +4 -1
  36. data/lib/rbgl/gui/backend.rb +21 -34
  37. data/lib/rbgl/gui/backend_factory.rb +89 -0
  38. data/lib/rbgl/gui/cocoa/backend.rb +59 -46
  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 +63 -0
  42. data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
  43. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  44. data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
  45. data/lib/rbgl/gui/file_backend.rb +50 -55
  46. data/lib/rbgl/gui/wayland/backend.rb +214 -60
  47. data/lib/rbgl/gui/wayland/codec.rb +50 -0
  48. data/lib/rbgl/gui/wayland/connection.rb +188 -231
  49. data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
  50. data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
  51. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
  53. data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  55. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
  56. data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
  57. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
  58. data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
  59. data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
  60. data/lib/rbgl/gui/window/render_loop.rb +111 -0
  61. data/lib/rbgl/gui/window.rb +74 -84
  62. data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
  63. data/lib/rbgl/gui/x11/backend.rb +73 -49
  64. data/lib/rbgl/gui/x11/connection.rb +284 -219
  65. data/lib/rbgl/gui/x11/event_parser.rb +71 -0
  66. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  67. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  68. data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
  69. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  70. data/lib/rbgl/gui/x11/transport.rb +81 -0
  71. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  72. data/lib/rbgl/gui.rb +3 -5
  73. data/lib/rbgl/version.rb +1 -1
  74. metadata +63 -24
  75. data/examples/chemical_heartbeat.rb +0 -166
  76. data/examples/dark_transit.rb +0 -166
  77. data/examples/fractured_orb.rb +0 -428
  78. data/examples/fractured_orb_rb.rb +0 -598
  79. data/examples/hexagonal_flow.rb +0 -333
  80. data/examples/teapot.rb +0 -362
  81. data/examples/teapot_mcu.rb +0 -344
@@ -1,35 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+ require_relative "file_backend/frame_writer"
5
+ require_relative "file_backend/ppm_writer"
6
+ require_relative "file_backend/bmp_writer"
7
+ require_relative "file_backend/png_writer"
8
+
3
9
  module RBGL
4
10
  module GUI
5
11
  class FileBackend < Backend
6
- def initialize(width, height, title = "RBGL", format: :ppm, output_dir: ".")
12
+ SUPPORTED_FORMATS = %i[ppm bmp png].freeze
13
+ PPM_MODES = %i[ascii binary].freeze
14
+
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)
7
18
  super(width, height, title)
8
- @format = format
9
- @output_dir = output_dir
19
+ @format = normalize_format(format)
20
+ @ppm_mode = normalize_ppm_mode(ppm_mode, @format)
21
+ @writer = FrameWriter.build(@format, ppm_mode: @ppm_mode)
22
+ @output_dir = File.expand_path(output_dir)
23
+ FileUtils.mkdir_p(@output_dir)
10
24
  @frame_count = 0
11
25
  @should_close = false
12
- @max_frames = nil
26
+ self.max_frames = max_frames
13
27
  end
14
28
 
15
29
  def present(framebuffer)
16
- filename = File.join(@output_dir, format("frame_%05d.#{@format}", @frame_count))
17
-
18
- case @format
19
- when :ppm
20
- File.write(filename, framebuffer.to_ppm)
21
- when :ppm_binary
22
- File.binwrite(filename, framebuffer.to_ppm_binary)
23
- when :bmp
24
- File.binwrite(filename, to_bmp(framebuffer))
25
- end
30
+ filename = File.join(@output_dir, format("frame_%05d.#{@writer.extension}", @frame_count))
31
+ @writer.write(filename, framebuffer)
26
32
 
27
33
  @frame_count += 1
28
34
 
29
35
  @should_close = true if @max_frames && @frame_count >= @max_frames
36
+ true
30
37
  end
31
38
 
32
39
  def poll_events
40
+ []
33
41
  end
34
42
 
35
43
  def should_close?
@@ -40,51 +48,38 @@ module RBGL
40
48
  @should_close = true
41
49
  end
42
50
 
43
- def set_max_frames(count)
44
- @max_frames = count
51
+ def max_frames=(count)
52
+ @max_frames = normalize_max_frames(count)
45
53
  end
46
54
 
55
+ alias set_max_frames max_frames=
56
+
47
57
  private
48
58
 
49
- def to_bmp(framebuffer)
50
- w = framebuffer.width
51
- h = framebuffer.height
52
- row_size = ((24 * w + 31) / 32) * 4
53
- pixel_data_size = row_size * h
54
- file_size = 54 + pixel_data_size
55
-
56
- header = [
57
- 0x42, 0x4D,
58
- file_size,
59
- 0, 0,
60
- 54
61
- ].pack("CCVvvV")
62
-
63
- dib = [
64
- 40,
65
- w, h,
66
- 1,
67
- 24,
68
- 0,
69
- pixel_data_size,
70
- 2835, 2835,
71
- 0, 0
72
- ].pack("VVVvvVVVVVV")
73
-
74
- pixels = +""
75
- (h - 1).downto(0) do |y|
76
- row = +""
77
- w.times do |x|
78
- color = framebuffer.get_pixel(x, y)
79
- bytes = color.to_bytes
80
- row << [bytes[2], bytes[1], bytes[0]].pack("CCC")
81
- end
82
- padding = row_size - w * 3
83
- row << "\x00" * padding
84
- pixels << row
85
- end
86
-
87
- header + dib + pixels
59
+ def normalize_format(format)
60
+ normalized = format.to_sym
61
+ return normalized if SUPPORTED_FORMATS.include?(normalized)
62
+
63
+ raise ArgumentError, "Unsupported file backend format: #{format}"
64
+ end
65
+
66
+ def normalize_ppm_mode(ppm_mode, format)
67
+ normalized = ppm_mode.to_sym
68
+ raise ArgumentError, "Unsupported PPM mode: #{ppm_mode}" unless PPM_MODES.include?(normalized)
69
+ return normalized if format == :ppm || normalized == :ascii
70
+
71
+ raise ArgumentError, "PPM mode is only supported with :ppm format"
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"
88
83
  end
89
84
  end
90
85
  end
@@ -1,16 +1,28 @@
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
7
8
  module Wayland
8
9
  class Backend < GUI::Backend
9
- def initialize(width, height, title = "RBGL")
10
- super
11
- @connection = Connection.new
12
- @windows = {}
10
+ BUFFER_WAIT_TIMEOUT = 0.25
11
+ BUFFER_POLL_INTERVAL = 0.016
12
+
13
+ def initialize(width, height, title = "RBGL", env: ENV, roundtrip_timeout: Connection::DEFAULT_ROUNDTRIP_TIMEOUT)
14
+ super(width, height, title)
15
+ @window = nil
16
+ @retired_buffers = []
17
+ @connection = Connection.new(env: env, roundtrip_timeout: roundtrip_timeout)
13
18
  setup_window(width, height, title)
19
+ rescue StandardError => error
20
+ begin
21
+ close
22
+ rescue StandardError
23
+ nil
24
+ end
25
+ raise error
14
26
  end
15
27
 
16
28
  private def setup_window(w, h, t)
@@ -19,72 +31,108 @@ module RBGL
19
31
  toplevel = xdg_surface.get_toplevel
20
32
  toplevel.set_title(t)
21
33
 
22
- shm_buffer = create_shm_buffer(w, h)
23
-
24
- surface.attach(shm_buffer, 0, 0)
25
- surface.commit
26
- @connection.flush
27
-
28
- handle = surface.id
29
- @windows[handle] = {
34
+ @window = {
30
35
  surface: surface,
31
36
  xdg_surface: xdg_surface,
32
37
  toplevel: toplevel,
33
- shm_buffer: shm_buffer,
38
+ buffers: [],
39
+ shm_buffer: nil,
34
40
  width: w,
35
41
  height: h,
36
- should_close: false,
37
- pending_events: []
42
+ should_close: false
38
43
  }
39
44
 
40
- @handle = handle
45
+ surface.commit
46
+ @connection.flush
47
+ wait_for_initial_configure(xdg_surface)
48
+
49
+ buffers = create_shm_buffers(w, h)
50
+ @window[:buffers] = buffers
51
+ @window[:shm_buffer] = buffers.first
41
52
  end
42
53
 
43
54
  def present(framebuffer)
44
- return unless @handle
55
+ return false unless @window
56
+
57
+ present_bytes(convert_to_wayland_format(framebuffer), framebuffer.width, framebuffer.height)
58
+ end
59
+
60
+ def set_pixels(buffer, width, height)
61
+ return false unless @window
62
+
63
+ bytes = validate_rgba_buffer(buffer, width, height)
64
+ present_bytes(rgba_to_wayland_bytes(bytes), width, height)
65
+ end
45
66
 
46
- window = @windows[@handle]
47
- return unless window
67
+ private def present_bytes(buffer, width, height)
68
+ window = @window
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
73
+
74
+ buffer_object = wait_for_available_buffer(window)
75
+ return false unless buffer_object
48
76
 
49
- buffer = convert_to_wayland_format(framebuffer)
50
- window[:shm_buffer].write(buffer)
77
+ buffer_object.write(buffer)
51
78
 
52
- window[:surface].damage(0, 0, framebuffer.width, framebuffer.height)
53
- window[:surface].attach(window[:shm_buffer], 0, 0)
79
+ window[:surface].damage(0, 0, width, height)
80
+ window[:surface].attach(buffer_object, 0, 0)
81
+ buffer_object.mark_in_use
82
+ window[:shm_buffer] = buffer_object
54
83
  window[:surface].commit
55
84
  @connection.flush
85
+ true
56
86
  end
57
87
 
58
88
  def poll_events
59
- events = []
60
- @connection.dispatch_pending
89
+ @connection.dispatch_pending.filter_map { |event| convert_event(event) }
90
+ end
61
91
 
62
- if @handle && @windows[@handle]
63
- events.concat(@windows[@handle][:pending_events])
64
- @windows[@handle][:pending_events] = []
65
- end
92
+ def resize(width, height)
93
+ window = @window
94
+ return super unless window
95
+ return super if window[:width] == width && window[:height] == height
66
96
 
67
- events
97
+ old_buffers = window.fetch(:buffers, [window[:shm_buffer]].compact)
98
+ new_buffers = create_shm_buffers(width, height)
99
+ window[:buffers] = new_buffers
100
+ window[:shm_buffer] = new_buffers.first
101
+ window[:width] = width
102
+ window[:height] = height
103
+ super
104
+ (@retired_buffers ||= []).concat(old_buffers)
105
+ old_buffers.each(&:destroy)
106
+ @retired_buffers.reject! { |buffer| buffer.respond_to?(:destroyed?) && buffer.destroyed? }
68
107
  end
69
108
 
70
109
  def should_close?
71
- return false unless @handle
72
-
73
- @windows[@handle]&.[](:should_close) || false
110
+ @window.nil? || @window[:should_close]
74
111
  end
75
112
 
76
113
  def close
77
- return unless @handle
78
-
79
- window = @windows[@handle]
80
- return unless window
114
+ window = @window
115
+ operations = []
116
+ if window
117
+ window[:should_close] = true
118
+ operations << -> { window[:toplevel].destroy }
119
+ operations << -> { window[:xdg_surface].destroy }
120
+ operations << -> { window[:surface].destroy }
121
+ buffers = window.fetch(:buffers, [window[:shm_buffer]].compact) + Array(@retired_buffers)
122
+ operations.concat(buffers.uniq.map { |buffer| -> { buffer.destroy(force: true) } })
123
+ operations << -> { @connection.flush }
124
+ end
125
+ operations << -> { @connection&.close }
81
126
 
82
- window[:should_close] = true
83
- window[:toplevel].destroy
84
- window[:xdg_surface].destroy
85
- window[:surface].destroy
86
- window[:shm_buffer].destroy
87
- @windows.delete(@handle)
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
88
136
  end
89
137
 
90
138
  private
@@ -93,33 +141,139 @@ module RBGL
93
141
  framebuffer.to_bgra_bytes
94
142
  end
95
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
+
155
+ def convert_event(raw)
156
+ window = window_for_event(raw[:surface_id] || raw[:object_id])
157
+ return nil unless window
158
+
159
+ case raw[:type]
160
+ when :xdg_toplevel_close
161
+ window[:should_close] = true
162
+ Event.new(:close)
163
+ when :xdg_toplevel_configure
164
+ width = raw[:width]
165
+ height = raw[:height]
166
+ return nil unless width.positive? && height.positive?
167
+
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])
179
+ end
180
+ end
181
+
182
+ def window_for_event(object_id)
183
+ return unless @window
184
+ return @window if @window[:toplevel].id == object_id || @window[:surface].id == object_id
185
+ end
186
+
187
+ def next_available_buffer(window)
188
+ window.fetch(:buffers, [window[:shm_buffer]].compact).find(&:available?)
189
+ end
190
+
191
+ def wait_for_available_buffer(window)
192
+ deadline = monotonic_time + BUFFER_WAIT_TIMEOUT
193
+
194
+ loop do
195
+ buffer = next_available_buffer(window)
196
+ return buffer if buffer
197
+ return nil if abort_buffer_wait?(window, deadline)
198
+
199
+ pump_connection(timeout: remaining_buffer_wait(deadline))
200
+ end
201
+ end
202
+
203
+ def pump_connection(timeout:)
204
+ if @connection.respond_to?(:pump_events)
205
+ @connection.pump_events(timeout: timeout)
206
+ else
207
+ sleep(timeout)
208
+ end
209
+ end
210
+
211
+ def abort_buffer_wait?(window, deadline)
212
+ window[:should_close] || monotonic_time >= deadline
213
+ end
214
+
215
+ def remaining_buffer_wait(deadline)
216
+ [deadline - monotonic_time, BUFFER_POLL_INTERVAL].min.clamp(0.0, BUFFER_POLL_INTERVAL)
217
+ end
218
+
219
+ def monotonic_time
220
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
221
+ end
222
+
223
+ def create_shm_buffers(width, height, count = 2)
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
236
+ end
237
+
96
238
  def create_shm_buffer(width, height)
97
239
  size = width * height * 4
98
240
 
99
- fd = create_anonymous_file(size)
241
+ file = create_anonymous_file(size)
242
+ pool = @connection.shm.create_pool(file, size)
243
+ buffer = pool.create_buffer(0, width, height, width * 4, :xrgb8888)
100
244
 
101
- pool = @connection.shm.create_pool(fd, size)
102
- buffer = pool.create_buffer(0, width, height, width * 4, :argb8888)
103
-
104
- ShmBuffer.new(fd, size, buffer)
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
105
259
  end
106
260
 
107
261
  def create_anonymous_file(size)
108
- name = "rbgl-#{Process.pid}-#{rand(10000)}"
109
- path = "/dev/shm/#{name}"
110
-
111
- file = File.open(path, File::RDWR | File::CREAT | File::EXCL, 0o600)
262
+ file = Tempfile.new("rbgl-wayland")
263
+ file.binmode
264
+ file.chmod(0o600)
112
265
  file.truncate(size)
113
- fd = file.fileno
114
- File.unlink(path)
266
+ file.unlink
267
+ file
268
+ end
269
+
270
+ def wait_for_initial_configure(xdg_surface)
271
+ configured = @connection.wait_until { xdg_surface.configured? }
272
+ return if configured
115
273
 
116
- fd
117
- rescue Errno::ENOENT
118
- require "tempfile"
119
- tmpfile = Tempfile.new("rbgl")
120
- tmpfile.truncate(size)
121
- tmpfile.fileno
274
+ raise GUI::BackendUnavailable, "Wayland compositor did not configure the surface"
122
275
  end
276
+
123
277
  end
124
278
  end
125
279
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module Wayland
6
+ class Codec
7
+ def pack_args(args)
8
+ result = String.new
9
+
10
+ args.each do |arg|
11
+ unless arg.is_a?(TypedArgument)
12
+ raise ArgumentError, "Wayland requests require typed arguments"
13
+ end
14
+
15
+ result << pack_typed_argument(arg)
16
+ end
17
+
18
+ result
19
+ end
20
+
21
+ def pad_length(length)
22
+ ((length + 3) / 4) * 4
23
+ end
24
+
25
+ private
26
+
27
+ def pack_typed_argument(argument)
28
+ case argument.type
29
+ when :uint, :object, :new_id
30
+ [argument.value].pack("V")
31
+ when :int
32
+ [argument.value].pack("l<")
33
+ when :fixed
34
+ [(argument.value * 256).round].pack("l<")
35
+ when :string
36
+ pack_string(argument.value)
37
+ else
38
+ raise ArgumentError, "Unsupported Wayland argument type: #{argument.type}"
39
+ end
40
+ end
41
+
42
+ def pack_string(value)
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))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end