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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class Backend
6
+ attr_reader :width, :height, :title
7
+
8
+ def initialize(width, height, title = "RBGL")
9
+ @width = width
10
+ @height = height
11
+ @title = title
12
+ @key_callback = nil
13
+ @mouse_callback = nil
14
+ @resize_callback = nil
15
+ end
16
+
17
+ def present(_framebuffer)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def poll_events
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def poll_events_raw
26
+ []
27
+ end
28
+
29
+ def should_close?
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def close
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def set_pixels(buffer, width, height)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def metal_available?
42
+ false
43
+ end
44
+
45
+ def native_handle
46
+ nil
47
+ end
48
+
49
+ def on_key(&block)
50
+ @key_callback = block
51
+ end
52
+
53
+ def on_mouse(&block)
54
+ @mouse_callback = block
55
+ end
56
+
57
+ def on_resize(&block)
58
+ @resize_callback = block
59
+ end
60
+
61
+ protected
62
+
63
+ def emit_key(key, action)
64
+ @key_callback&.call(key, action)
65
+ end
66
+
67
+ def emit_mouse(x, y, button, action)
68
+ @mouse_callback&.call(x, y, button, action)
69
+ end
70
+
71
+ def emit_resize(width, height)
72
+ @resize_callback&.call(width, height)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "metaco"
5
+ METACO_AVAILABLE = true
6
+ rescue LoadError
7
+ METACO_AVAILABLE = false
8
+ end
9
+
10
+ module RBGL
11
+ module GUI
12
+ module Cocoa
13
+ class Backend < GUI::Backend
14
+ def initialize(width, height, title = "RBGL")
15
+ unless METACO_AVAILABLE
16
+ raise LoadError, "metaco gem is required for Cocoa backend. Install it with: gem install metaco"
17
+ end
18
+
19
+ super
20
+ Metaco.init
21
+ @handle = Metaco.window_create(width, height, title)
22
+ end
23
+
24
+ def present(framebuffer)
25
+ return unless @handle
26
+
27
+ Metaco.set_pixels(@handle, framebuffer.to_rgba_bytes, framebuffer.width, framebuffer.height)
28
+ Metaco.present(@handle)
29
+ end
30
+
31
+ def poll_events
32
+ return [] unless @handle
33
+
34
+ raw_events = Metaco.poll_events(@handle)
35
+ events = []
36
+
37
+ raw_events.each do |e|
38
+ event = convert_event(e)
39
+ if event
40
+ events << event
41
+ emit_from_event(event)
42
+ end
43
+ end
44
+
45
+ events
46
+ end
47
+
48
+ def poll_events_raw
49
+ return [] unless @handle
50
+
51
+ Metaco.poll_events(@handle)
52
+ end
53
+
54
+ def should_close?
55
+ return false unless @handle
56
+
57
+ Metaco.should_close?(@handle)
58
+ end
59
+
60
+ def close
61
+ return unless @handle
62
+
63
+ Metaco.window_destroy(@handle)
64
+ @handle = nil
65
+ end
66
+
67
+ def set_pixels(buffer, width, height)
68
+ return unless @handle
69
+
70
+ Metaco.set_pixels(@handle, buffer, width, height)
71
+ Metaco.present(@handle)
72
+ end
73
+
74
+ def metal_available?
75
+ return false unless @handle
76
+
77
+ Metaco.metal_compute_available?(@handle)
78
+ end
79
+
80
+ def native_handle
81
+ @handle
82
+ end
83
+
84
+ private
85
+
86
+ def convert_event(raw)
87
+ type = raw[:type]
88
+
89
+ case type
90
+ when :key_press
91
+ Event.new(:key_press, key: raw[:key], char: raw[:char])
92
+ when :key_release
93
+ Event.new(:key_release, key: raw[:key])
94
+ when :mouse_press
95
+ Event.new(:mouse_press, x: raw[:x], y: raw[:y], button: raw[:button])
96
+ when :mouse_release
97
+ Event.new(:mouse_release, x: raw[:x], y: raw[:y], button: raw[:button])
98
+ when :mouse_move
99
+ Event.new(:mouse_move, x: raw[:x], y: raw[:y])
100
+ else
101
+ nil
102
+ end
103
+ end
104
+
105
+ def emit_from_event(event)
106
+ case event.type
107
+ when :key_press, :key_release
108
+ emit_key(event.key, event.type == :key_press ? :press : :release)
109
+ when :mouse_press, :mouse_release, :mouse_move
110
+ action = case event.type
111
+ when :mouse_press then :press
112
+ when :mouse_release then :release
113
+ else :move
114
+ end
115
+ emit_mouse(event.x, event.y, event[:button], action)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class Event
6
+ attr_reader :type, :data
7
+
8
+ def initialize(type, **data)
9
+ @type = type
10
+ @data = data
11
+ end
12
+
13
+ def [](key)
14
+ @data[key]
15
+ end
16
+
17
+ def method_missing(name, *args)
18
+ @data.key?(name) ? @data[name] : super
19
+ end
20
+
21
+ def respond_to_missing?(name, include_private = false)
22
+ @data.key?(name) || super
23
+ end
24
+
25
+ def to_h
26
+ { type: @type }.merge(@data)
27
+ end
28
+
29
+ def inspect
30
+ "Event[#{@type}, #{@data.map { |k, v| "#{k}: #{v}" }.join(', ')}]"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class FileBackend < Backend
6
+ def initialize(width, height, title = "RBGL", format: :ppm, output_dir: ".")
7
+ super(width, height, title)
8
+ @format = format
9
+ @output_dir = output_dir
10
+ @frame_count = 0
11
+ @should_close = false
12
+ @max_frames = nil
13
+ end
14
+
15
+ 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
26
+
27
+ @frame_count += 1
28
+
29
+ @should_close = true if @max_frames && @frame_count >= @max_frames
30
+ end
31
+
32
+ def poll_events
33
+ end
34
+
35
+ def should_close?
36
+ @should_close
37
+ end
38
+
39
+ def close
40
+ @should_close = true
41
+ end
42
+
43
+ def set_max_frames(count)
44
+ @max_frames = count
45
+ end
46
+
47
+ private
48
+
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
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "connection"
4
+
5
+ module RBGL
6
+ module GUI
7
+ module Wayland
8
+ class Backend < GUI::Backend
9
+ def initialize(width, height, title = "RBGL")
10
+ super
11
+ @connection = Connection.new
12
+ @windows = {}
13
+ setup_window(width, height, title)
14
+ end
15
+
16
+ private def setup_window(w, h, t)
17
+ surface = @connection.compositor.create_surface
18
+ xdg_surface = @connection.xdg_wm_base.get_xdg_surface(surface)
19
+ toplevel = xdg_surface.get_toplevel
20
+ toplevel.set_title(t)
21
+
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] = {
30
+ surface: surface,
31
+ xdg_surface: xdg_surface,
32
+ toplevel: toplevel,
33
+ shm_buffer: shm_buffer,
34
+ width: w,
35
+ height: h,
36
+ should_close: false,
37
+ pending_events: []
38
+ }
39
+
40
+ @handle = handle
41
+ end
42
+
43
+ def present(framebuffer)
44
+ return unless @handle
45
+
46
+ window = @windows[@handle]
47
+ return unless window
48
+
49
+ buffer = convert_to_wayland_format(framebuffer)
50
+ window[:shm_buffer].write(buffer)
51
+
52
+ window[:surface].damage(0, 0, framebuffer.width, framebuffer.height)
53
+ window[:surface].attach(window[:shm_buffer], 0, 0)
54
+ window[:surface].commit
55
+ @connection.flush
56
+ end
57
+
58
+ def poll_events
59
+ events = []
60
+ @connection.dispatch_pending
61
+
62
+ if @handle && @windows[@handle]
63
+ events.concat(@windows[@handle][:pending_events])
64
+ @windows[@handle][:pending_events] = []
65
+ end
66
+
67
+ events
68
+ end
69
+
70
+ def should_close?
71
+ return false unless @handle
72
+
73
+ @windows[@handle]&.[](:should_close) || false
74
+ end
75
+
76
+ def close
77
+ return unless @handle
78
+
79
+ window = @windows[@handle]
80
+ return unless window
81
+
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)
88
+ end
89
+
90
+ private
91
+
92
+ def convert_to_wayland_format(framebuffer)
93
+ framebuffer.to_bgra_bytes
94
+ end
95
+
96
+ def create_shm_buffer(width, height)
97
+ size = width * height * 4
98
+
99
+ fd = create_anonymous_file(size)
100
+
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)
105
+ end
106
+
107
+ 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)
112
+ file.truncate(size)
113
+ fd = file.fileno
114
+ File.unlink(path)
115
+
116
+ fd
117
+ rescue Errno::ENOENT
118
+ require "tempfile"
119
+ tmpfile = Tempfile.new("rbgl")
120
+ tmpfile.truncate(size)
121
+ tmpfile.fileno
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end