rbgl 0.1.0

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +21 -0
  4. data/README.md +123 -0
  5. data/Rakefile +12 -0
  6. data/examples/array_test.rb +99 -0
  7. data/examples/chemical_heartbeat.rb +166 -0
  8. data/examples/color_test.rb +61 -0
  9. data/examples/cube_spinning.rb +87 -0
  10. data/examples/dark_transit.rb +166 -0
  11. data/examples/fractured_orb.rb +428 -0
  12. data/examples/fractured_orb_rb.rb +598 -0
  13. data/examples/gradient.rb +84 -0
  14. data/examples/hexagonal_flow.rb +333 -0
  15. data/examples/multi_return_test.rb +98 -0
  16. data/examples/plasma.rb +78 -0
  17. data/examples/sphere_raymarch.rb +126 -0
  18. data/examples/teapot.rb +362 -0
  19. data/examples/teapot_mcu.rb +344 -0
  20. data/examples/triangle_basic.rb +36 -0
  21. data/examples/triangle_window.rb +62 -0
  22. data/examples/window_test.rb +36 -0
  23. data/lib/rbgl/engine/buffer.rb +160 -0
  24. data/lib/rbgl/engine/context.rb +157 -0
  25. data/lib/rbgl/engine/framebuffer.rb +115 -0
  26. data/lib/rbgl/engine/pipeline.rb +35 -0
  27. data/lib/rbgl/engine/rasterizer.rb +213 -0
  28. data/lib/rbgl/engine/shader.rb +324 -0
  29. data/lib/rbgl/engine/texture.rb +125 -0
  30. data/lib/rbgl/engine.rb +15 -0
  31. data/lib/rbgl/gui/backend.rb +76 -0
  32. data/lib/rbgl/gui/cocoa/backend.rb +121 -0
  33. data/lib/rbgl/gui/event.rb +34 -0
  34. data/lib/rbgl/gui/file_backend.rb +91 -0
  35. data/lib/rbgl/gui/wayland/backend.rb +126 -0
  36. data/lib/rbgl/gui/wayland/connection.rb +331 -0
  37. data/lib/rbgl/gui/window.rb +148 -0
  38. data/lib/rbgl/gui/x11/backend.rb +156 -0
  39. data/lib/rbgl/gui/x11/connection.rb +344 -0
  40. data/lib/rbgl/gui.rb +16 -0
  41. data/lib/rbgl/version.rb +5 -0
  42. data/lib/rbgl.rb +6 -0
  43. metadata +114 -0
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module RBGL
6
+ module GUI
7
+ module Wayland
8
+ class WaylandObject
9
+ attr_reader :id, :connection
10
+
11
+ def initialize(connection, id)
12
+ @connection = connection
13
+ @id = id
14
+ end
15
+
16
+ def send_request(opcode, *args)
17
+ @connection.send_request(@id, opcode, *args)
18
+ end
19
+ end
20
+
21
+ class Display < WaylandObject
22
+ def initialize(connection)
23
+ super(connection, 1)
24
+ end
25
+
26
+ def sync
27
+ callback_id = @connection.allocate_id
28
+ send_request(0, callback_id)
29
+ Callback.new(@connection, callback_id)
30
+ end
31
+
32
+ def get_registry
33
+ registry_id = @connection.allocate_id
34
+ send_request(1, registry_id)
35
+ Registry.new(@connection, registry_id)
36
+ end
37
+ end
38
+
39
+ class Registry < WaylandObject
40
+ def bind(name, interface, version)
41
+ new_id = @connection.allocate_id
42
+ send_request(0, name, interface, version, new_id)
43
+ new_id
44
+ end
45
+ end
46
+
47
+ class Callback < WaylandObject
48
+ def initialize(connection, id)
49
+ super
50
+ @done = false
51
+ end
52
+
53
+ def done?
54
+ @done
55
+ end
56
+
57
+ def handle_done
58
+ @done = true
59
+ end
60
+ end
61
+
62
+ class Compositor < WaylandObject
63
+ def create_surface
64
+ surface_id = @connection.allocate_id
65
+ send_request(0, surface_id)
66
+ Surface.new(@connection, surface_id)
67
+ end
68
+ end
69
+
70
+ class Surface < WaylandObject
71
+ def attach(buffer, x, y)
72
+ send_request(1, buffer.id, x, y)
73
+ end
74
+
75
+ def damage(x, y, width, height)
76
+ send_request(2, x, y, width, height)
77
+ end
78
+
79
+ def commit
80
+ send_request(6)
81
+ end
82
+
83
+ def destroy
84
+ send_request(0)
85
+ end
86
+ end
87
+
88
+ class Shm < WaylandObject
89
+ def create_pool(fd, size)
90
+ pool_id = @connection.allocate_id
91
+ @connection.send_request_with_fd(@id, 0, pool_id, size, fd)
92
+ ShmPool.new(@connection, pool_id)
93
+ end
94
+ end
95
+
96
+ class ShmPool < WaylandObject
97
+ def create_buffer(offset, width, height, stride, format)
98
+ buffer_id = @connection.allocate_id
99
+ format_val = case format
100
+ when :argb8888 then 0
101
+ when :xrgb8888 then 1
102
+ else 0
103
+ end
104
+ send_request(0, buffer_id, offset, width, height, stride, format_val)
105
+ WlBuffer.new(@connection, buffer_id)
106
+ end
107
+
108
+ def destroy
109
+ send_request(1)
110
+ end
111
+ end
112
+
113
+ class WlBuffer < WaylandObject
114
+ def destroy
115
+ send_request(0)
116
+ end
117
+ end
118
+
119
+ class XdgWmBase < WaylandObject
120
+ def get_xdg_surface(surface)
121
+ xdg_surface_id = @connection.allocate_id
122
+ send_request(2, xdg_surface_id, surface.id)
123
+ XdgSurface.new(@connection, xdg_surface_id)
124
+ end
125
+
126
+ def pong(serial)
127
+ send_request(3, serial)
128
+ end
129
+ end
130
+
131
+ class XdgSurface < WaylandObject
132
+ def get_toplevel
133
+ toplevel_id = @connection.allocate_id
134
+ send_request(1, toplevel_id)
135
+ XdgToplevel.new(@connection, toplevel_id)
136
+ end
137
+
138
+ def ack_configure(serial)
139
+ send_request(4, serial)
140
+ end
141
+
142
+ def destroy
143
+ send_request(0)
144
+ end
145
+ end
146
+
147
+ class XdgToplevel < WaylandObject
148
+ def set_title(title)
149
+ send_request(2, title)
150
+ end
151
+
152
+ def destroy
153
+ send_request(0)
154
+ end
155
+ end
156
+
157
+ class Connection
158
+ attr_reader :compositor, :shm, :xdg_wm_base
159
+
160
+ def initialize
161
+ socket_path = ENV["WAYLAND_DISPLAY"] || "wayland-0"
162
+ unless socket_path.start_with?("/")
163
+ runtime_dir = ENV["XDG_RUNTIME_DIR"] || "/run/user/#{Process.uid}"
164
+ socket_path = File.join(runtime_dir, socket_path)
165
+ end
166
+
167
+ @socket = UNIXSocket.new(socket_path)
168
+ @objects = {}
169
+ @next_id = 2
170
+ @globals = {}
171
+
172
+ @display = Display.new(self)
173
+ @objects[1] = @display
174
+
175
+ registry = @display.get_registry
176
+ @objects[registry.id] = registry
177
+ flush
178
+ roundtrip
179
+
180
+ bind_globals
181
+ end
182
+
183
+ def allocate_id
184
+ id = @next_id
185
+ @next_id += 1
186
+ id
187
+ end
188
+
189
+ def send_request(object_id, opcode, *args)
190
+ payload = pack_args(args)
191
+ header = [object_id, (payload.bytesize + 8) << 16 | opcode].pack("VV")
192
+ @socket.write(header + payload)
193
+ end
194
+
195
+ def send_request_with_fd(object_id, opcode, *args, fd)
196
+ payload = pack_args(args)
197
+ header = [object_id, (payload.bytesize + 8) << 16 | opcode].pack("VV")
198
+
199
+ @socket.sendmsg(header + payload, 0, nil, Socket::AncillaryData.unix_rights(fd))
200
+ end
201
+
202
+ def flush
203
+ @socket.flush
204
+ end
205
+
206
+ def dispatch_pending
207
+ while IO.select([@socket], nil, nil, 0)
208
+ header = @socket.read(8)
209
+ break unless header && header.bytesize == 8
210
+
211
+ object_id, size_and_opcode = header.unpack("VV")
212
+ size = size_and_opcode >> 16
213
+ opcode = size_and_opcode & 0xFFFF
214
+
215
+ payload = size > 8 ? @socket.read(size - 8) : ""
216
+ handle_event(object_id, opcode, payload)
217
+ end
218
+ end
219
+
220
+ def roundtrip
221
+ callback = @display.sync
222
+ @objects[callback.id] = callback
223
+ flush
224
+
225
+ until callback.done?
226
+ dispatch_pending
227
+ sleep 0.001
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def handle_event(object_id, opcode, payload)
234
+ case object_id
235
+ when 2
236
+ if opcode == 0
237
+ name = payload[0, 4].unpack1("V")
238
+ interface_len = payload[4, 4].unpack1("V")
239
+ interface = payload[8, interface_len - 1]
240
+ version = payload[8 + pad_length(interface_len), 4].unpack1("V")
241
+ @globals[interface] = { name: name, version: version }
242
+ end
243
+ else
244
+ obj = @objects[object_id]
245
+ if obj.is_a?(Callback) && opcode == 0
246
+ obj.handle_done
247
+ end
248
+ end
249
+ end
250
+
251
+ def bind_globals
252
+ if @globals["wl_compositor"]
253
+ id = allocate_id
254
+ g = @globals["wl_compositor"]
255
+ @objects[2].bind(g[:name], "wl_compositor", [g[:version], 4].min)
256
+ @compositor = Compositor.new(self, id)
257
+ @objects[id] = @compositor
258
+ end
259
+
260
+ if @globals["wl_shm"]
261
+ id = allocate_id
262
+ g = @globals["wl_shm"]
263
+ @objects[2].bind(g[:name], "wl_shm", [g[:version], 1].min)
264
+ @shm = Shm.new(self, id)
265
+ @objects[id] = @shm
266
+ end
267
+
268
+ if @globals["xdg_wm_base"]
269
+ id = allocate_id
270
+ g = @globals["xdg_wm_base"]
271
+ @objects[2].bind(g[:name], "xdg_wm_base", [g[:version], 2].min)
272
+ @xdg_wm_base = XdgWmBase.new(self, id)
273
+ @objects[id] = @xdg_wm_base
274
+ end
275
+
276
+ flush
277
+ roundtrip
278
+ end
279
+
280
+ def pack_args(args)
281
+ result = String.new
282
+ args.each do |arg|
283
+ case arg
284
+ when Integer
285
+ result << [arg].pack("V")
286
+ when String
287
+ len = arg.bytesize + 1
288
+ result << [len].pack("V")
289
+ result << arg << "\x00"
290
+ result << "\x00" * ((4 - len % 4) % 4)
291
+ when Float
292
+ result << [(arg * 256).to_i].pack("V")
293
+ end
294
+ end
295
+ result
296
+ end
297
+
298
+ def pad_length(len)
299
+ ((len + 3) / 4) * 4
300
+ end
301
+ end
302
+
303
+ class ShmBuffer
304
+ attr_reader :wl_buffer
305
+
306
+ def initialize(fd, size, wl_buffer)
307
+ @fd = fd
308
+ @size = size
309
+ @wl_buffer = wl_buffer
310
+ @file = File.open("/proc/self/fd/#{fd}", "r+b")
311
+ @file.seek(0)
312
+ end
313
+
314
+ def write(data)
315
+ @file.seek(0)
316
+ @file.write(data)
317
+ @file.flush
318
+ end
319
+
320
+ def id
321
+ @wl_buffer.id
322
+ end
323
+
324
+ def destroy
325
+ @wl_buffer.destroy
326
+ @file.close
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class Window
6
+ attr_reader :context, :backend, :width, :height
7
+
8
+ def initialize(width:, height:, title: "RBGL", backend: :auto, **options)
9
+ @width = width
10
+ @height = height
11
+ @title = title
12
+ @context = Engine::Context.new(width: width, height: height)
13
+
14
+ @backend = case backend
15
+ when :auto, :native
16
+ detect_backend(width, height, title)
17
+ when :file
18
+ FileBackend.new(width, height, title, **options)
19
+ when :x11
20
+ require_relative "x11/backend"
21
+ X11::Backend.new(width, height, title)
22
+ when :wayland
23
+ require_relative "wayland/backend"
24
+ Wayland::Backend.new(width, height, title)
25
+ when :cocoa
26
+ require_relative "cocoa/backend"
27
+ Cocoa::Backend.new(width, height, title)
28
+ when Backend
29
+ backend
30
+ else
31
+ raise "Unknown backend: #{backend}"
32
+ end
33
+
34
+ @running = false
35
+ @frame_callback = nil
36
+ @last_time = Time.now
37
+ @fps = 0
38
+ @frame_count = 0
39
+ @event_handlers = Hash.new { |h, k| h[k] = [] }
40
+ end
41
+
42
+ def on(event_type, &block)
43
+ @event_handlers[event_type] << block
44
+ end
45
+
46
+ def on_key(&block)
47
+ @backend.on_key(&block)
48
+ end
49
+
50
+ def on_mouse(&block)
51
+ @backend.on_mouse(&block)
52
+ end
53
+
54
+ def run(&frame_callback)
55
+ @frame_callback = frame_callback
56
+ @running = true
57
+ @start_time = Time.now
58
+
59
+ while @running && !@backend.should_close?
60
+ current_time = Time.now
61
+ delta_time = current_time - @last_time
62
+ @last_time = current_time
63
+
64
+ process_events
65
+
66
+ @frame_callback&.call(@context, delta_time)
67
+
68
+ @backend.present(@context.framebuffer)
69
+
70
+ @frame_count += 1
71
+ elapsed = current_time - @start_time
72
+ @fps = @frame_count / elapsed if elapsed > 0
73
+ end
74
+
75
+ @backend.close
76
+ end
77
+
78
+ def stop
79
+ @running = false
80
+ end
81
+
82
+ def present_framebuffer(framebuffer = nil)
83
+ fb = framebuffer || @context.framebuffer
84
+ @backend.present(fb)
85
+ end
86
+
87
+ def set_pixels(buffer)
88
+ @backend.set_pixels(buffer, @width, @height)
89
+ end
90
+
91
+ def metal_available?
92
+ @backend.metal_available?
93
+ end
94
+
95
+ def native_handle
96
+ @backend.native_handle
97
+ end
98
+
99
+ def should_close?
100
+ @backend.should_close?
101
+ end
102
+
103
+ def poll_events_raw
104
+ @backend.poll_events_raw
105
+ end
106
+
107
+ def close
108
+ @backend.close
109
+ end
110
+
111
+ attr_reader :fps
112
+
113
+ private
114
+
115
+ def detect_backend(width, height, title)
116
+ case RUBY_PLATFORM
117
+ when /darwin/
118
+ require_relative "cocoa/backend"
119
+ Cocoa::Backend.new(width, height, title)
120
+ when /linux/
121
+ if ENV["WAYLAND_DISPLAY"]
122
+ require_relative "wayland/backend"
123
+ Wayland::Backend.new(width, height, title)
124
+ elsif ENV["DISPLAY"]
125
+ require_relative "x11/backend"
126
+ X11::Backend.new(width, height, title)
127
+ else
128
+ raise "No display server found (DISPLAY or WAYLAND_DISPLAY not set)"
129
+ end
130
+ else
131
+ raise "Unsupported platform: #{RUBY_PLATFORM}"
132
+ end
133
+ end
134
+
135
+ def process_events
136
+ events = @backend.poll_events
137
+
138
+ return unless events.is_a?(Array)
139
+
140
+ events.each do |event|
141
+ next unless event.is_a?(Event)
142
+
143
+ @event_handlers[event.type].each { |handler| handler.call(event) }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "connection"
4
+
5
+ module RBGL
6
+ module GUI
7
+ module X11
8
+ class Backend < GUI::Backend
9
+ def initialize(width, height, title = "RBGL")
10
+ super
11
+ @display = Connection.new(ENV["DISPLAY"] || ":0")
12
+ @windows = {}
13
+ setup_window(width, height, title)
14
+ end
15
+
16
+ private def setup_window(w, h, t)
17
+ wid = @display.generate_id
18
+
19
+ @display.create_window(
20
+ depth: @display.root_depth,
21
+ wid: wid,
22
+ parent: @display.root,
23
+ x: 0, y: 0,
24
+ width: w, height: h,
25
+ border_width: 0,
26
+ window_class: :input_output,
27
+ visual: @display.root_visual,
28
+ value_mask: [:back_pixel, :event_mask],
29
+ values: {
30
+ back_pixel: @display.black_pixel,
31
+ event_mask: [:exposure, :key_press, :key_release,
32
+ :button_press, :button_release, :pointer_motion,
33
+ :structure_notify]
34
+ }
35
+ )
36
+
37
+ @display.change_property(wid, :wm_name, :string, t)
38
+ @display.map_window(wid)
39
+ @display.flush
40
+
41
+ gc_id = @display.generate_id
42
+ @display.create_gc(gc_id, wid)
43
+
44
+ @windows[wid] = {
45
+ width: w,
46
+ height: h,
47
+ gc: gc_id,
48
+ should_close: false
49
+ }
50
+
51
+ @handle = wid
52
+ end
53
+
54
+ def present(framebuffer)
55
+ return unless @handle
56
+
57
+ window = @windows[@handle]
58
+ return unless window
59
+
60
+ buffer = convert_to_x11_format(framebuffer)
61
+
62
+ @display.put_image(
63
+ format: :z_pixmap,
64
+ drawable: @handle,
65
+ gc: window[:gc],
66
+ width: framebuffer.width,
67
+ height: framebuffer.height,
68
+ dst_x: 0, dst_y: 0,
69
+ depth: @display.root_depth,
70
+ data: buffer
71
+ )
72
+
73
+ @display.flush
74
+ end
75
+
76
+ def poll_events
77
+ events = []
78
+
79
+ while @display.pending > 0
80
+ raw_event = @display.next_event
81
+ next unless raw_event
82
+
83
+ event = convert_event(raw_event)
84
+ if event
85
+ events << event
86
+ emit_from_event(event)
87
+ end
88
+ end
89
+
90
+ events
91
+ end
92
+
93
+ def should_close?
94
+ return false unless @handle
95
+
96
+ @windows[@handle]&.[](:should_close) || false
97
+ end
98
+
99
+ def close
100
+ return unless @handle
101
+
102
+ @windows[@handle][:should_close] = true
103
+ @display.destroy_window(@handle)
104
+ @windows.delete(@handle)
105
+ end
106
+
107
+ private
108
+
109
+ def convert_to_x11_format(framebuffer)
110
+ # X11 uses BGRX format (blue, green, red, padding)
111
+ framebuffer.to_bgra_bytes
112
+ end
113
+
114
+ def convert_event(raw)
115
+ case raw[:type]
116
+ when :key_press
117
+ Event.new(:key_press, key: raw[:keycode])
118
+ when :key_release
119
+ Event.new(:key_release, key: raw[:keycode])
120
+ when :button_press
121
+ Event.new(:mouse_press, x: raw[:x], y: raw[:y], button: raw[:button])
122
+ when :button_release
123
+ Event.new(:mouse_release, x: raw[:x], y: raw[:y], button: raw[:button])
124
+ when :motion_notify
125
+ Event.new(:mouse_move, x: raw[:x], y: raw[:y])
126
+ when :configure_notify
127
+ Event.new(:resize, width: raw[:width], height: raw[:height])
128
+ when :client_message
129
+ if @handle && @windows[@handle]
130
+ @windows[@handle][:should_close] = true
131
+ end
132
+ Event.new(:close)
133
+ else
134
+ nil
135
+ end
136
+ end
137
+
138
+ def emit_from_event(event)
139
+ case event.type
140
+ when :key_press, :key_release
141
+ emit_key(event.key, event.type == :key_press ? :press : :release)
142
+ when :mouse_press, :mouse_release, :mouse_move
143
+ action = case event.type
144
+ when :mouse_press then :press
145
+ when :mouse_release then :release
146
+ else :move
147
+ end
148
+ emit_mouse(event.x, event.y, event[:button], action)
149
+ when :resize
150
+ emit_resize(event.width, event.height)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end