rbgl 1.0.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +7 -1
  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 +49 -8
  20. data/lib/rbgl/engine/clip_space_clipper.rb +23 -3
  21. data/lib/rbgl/engine/context.rb +23 -6
  22. data/lib/rbgl/engine/framebuffer.rb +137 -41
  23. data/lib/rbgl/engine/immutable_color.rb +32 -0
  24. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +100 -10
  25. data/lib/rbgl/engine/rasterizer/line_renderer.rb +30 -14
  26. data/lib/rbgl/engine/rasterizer/point_renderer.rb +3 -3
  27. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +72 -23
  28. data/lib/rbgl/engine/rasterizer.rb +3 -20
  29. data/lib/rbgl/engine/shader/base_shader.rb +3 -7
  30. data/lib/rbgl/engine/shader/builtins.rb +13 -10
  31. data/lib/rbgl/engine/shader/dynamic_data.rb +18 -10
  32. data/lib/rbgl/engine/texture.rb +74 -50
  33. data/lib/rbgl/engine.rb +3 -1
  34. data/lib/rbgl/gui/backend.rb +12 -7
  35. data/lib/rbgl/gui/backend_factory.rb +7 -3
  36. data/lib/rbgl/gui/cocoa/backend.rb +50 -17
  37. data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
  38. data/lib/rbgl/gui/event.rb +4 -2
  39. data/lib/rbgl/gui/file_backend/bmp_writer.rb +2 -2
  40. data/lib/rbgl/gui/file_backend/frame_writer.rb +7 -0
  41. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  42. data/lib/rbgl/gui/file_backend/ppm_writer.rb +1 -0
  43. data/lib/rbgl/gui/file_backend.rb +24 -6
  44. data/lib/rbgl/gui/wayland/backend.rb +127 -55
  45. data/lib/rbgl/gui/wayland/codec.rb +7 -11
  46. data/lib/rbgl/gui/wayland/connection.rb +137 -20
  47. data/lib/rbgl/gui/wayland/event_dispatcher.rb +103 -12
  48. data/lib/rbgl/gui/wayland/global_binder.rb +6 -4
  49. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  50. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +14 -2
  51. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +23 -12
  53. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +15 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects.rb +1 -0
  55. data/lib/rbgl/gui/window/render_loop.rb +71 -18
  56. data/lib/rbgl/gui/window.rb +39 -8
  57. data/lib/rbgl/gui/x11/atom_cache.rb +4 -1
  58. data/lib/rbgl/gui/x11/backend.rb +66 -28
  59. data/lib/rbgl/gui/x11/connection.rb +216 -86
  60. data/lib/rbgl/gui/x11/event_parser.rb +13 -2
  61. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  62. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  63. data/lib/rbgl/gui/x11/request_encoder.rb +29 -28
  64. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  65. data/lib/rbgl/gui/x11/transport.rb +52 -2
  66. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  67. data/lib/rbgl/gui.rb +2 -5
  68. data/lib/rbgl/version.rb +1 -1
  69. metadata +36 -24
  70. data/examples/chemical_heartbeat.rb +0 -166
  71. data/examples/dark_transit.rb +0 -166
  72. data/examples/fractured_orb.rb +0 -428
  73. data/examples/fractured_orb_rb.rb +0 -598
  74. data/examples/hexagonal_flow.rb +0 -333
  75. data/examples/teapot.rb +0 -362
  76. data/examples/teapot_mcu.rb +0 -344
@@ -76,31 +76,14 @@ module RBGL
76
76
  end
77
77
 
78
78
  Larb::Vec3.new(
79
- (ndc.x + 1) * 0.5 * @viewport[:width] + @viewport[:x],
80
- (1 - ndc.y) * 0.5 * @viewport[:height] + @viewport[:y],
79
+ ((ndc.x + 1) * 0.5 * @viewport[:width]) + @viewport[:x],
80
+ ((1 - ndc.y) * 0.5 * @viewport[:height]) + @viewport[:y],
81
81
  (ndc.z + 1) * 0.5
82
82
  )
83
83
  end
84
84
 
85
85
  def edge_function(a, b, c)
86
- (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)
87
- end
88
-
89
- def interpolate_attributes(v0, v1, v2, w0, w1, w2)
90
- @attribute_interpolator.interpolate([v0, v1, v2], [w0, w1, w2])
91
- end
92
-
93
- def interpolate_value(a, b, c, w0, w1, w2)
94
- @attribute_interpolator.interpolate_values([a, b, c], [w0, w1, w2])
95
- end
96
-
97
- def interpolate_line_attributes(v0, v1, t)
98
- corrected_weights = perspective_correct_weights([v0[:position], v1[:position]], [1.0 - t, t])
99
- @attribute_interpolator.interpolate([v0, v1], corrected_weights)
100
- end
101
-
102
- def perspective_correct_weights(positions, weights)
103
- @attribute_interpolator.perspective_correct_weights(positions, weights)
86
+ ((c.x - a.x) * (b.y - a.y)) - ((c.y - a.y) * (b.x - a.x))
104
87
  end
105
88
 
106
89
  def state(cull_mode:, depth_test:, depth_write:, blend_mode:)
@@ -9,18 +9,14 @@ module RBGL
9
9
  @process_block = block
10
10
  end
11
11
 
12
- def process(input, uniforms)
13
- output = ShaderIO.new
14
- @input = input
15
- @uniforms = uniforms
16
- @output = output
12
+ def process(input, uniforms, output: nil)
13
+ output ||= ShaderIO.new
14
+ output.clear
17
15
 
18
16
  instance_exec(input, uniforms, output, &@process_block)
19
17
  finalize_output(output)
20
18
  end
21
19
 
22
- attr_reader :input, :uniforms, :output
23
-
24
20
  def self.create(&block)
25
21
  new(&block)
26
22
  end
@@ -53,25 +53,26 @@ module RBGL
53
53
 
54
54
  def refract(v, n, eta)
55
55
  cos_i = -dot(n, v)
56
- sin_t2 = eta * eta * (1.0 - cos_i * cos_i)
57
- return vec3(0) if sin_t2 > 1.0
56
+ sin_t2 = eta * eta * (1.0 - (cos_i * cos_i))
57
+ return v * 0 if sin_t2 > 1.0
58
58
 
59
59
  cos_t = Math.sqrt(1.0 - sin_t2)
60
- v * eta + n * (eta * cos_i - cos_t)
60
+ (v * eta) + (n * ((eta * cos_i) - cos_t))
61
61
  end
62
62
 
63
63
  def mix(a, b, t)
64
64
  case a
65
- when Numeric then a + (b - a) * t
66
- when Larb::Vec2, Larb::Vec3, Larb::Vec4 then a.lerp(b, t)
67
- when Larb::Color then a.lerp(b, t)
65
+ when Numeric then a + ((b - a) * t)
66
+ when Larb::Vec2, Larb::Vec3, Larb::Vec4, Larb::Color then a.lerp(b, t)
67
+ else
68
+ raise ArgumentError, "Unsupported mix operand: #{a.class}"
68
69
  end
69
70
  end
70
71
 
71
72
  def clamp(v, min_val, max_val)
72
73
  case v
73
74
  when Numeric then v.clamp(min_val, max_val)
74
- when Larb::Color then v.clamp
75
+ when Larb::Color then Larb::Color.new(*v.to_a.map { |component| component.clamp(min_val, max_val) })
75
76
  else
76
77
  component_map(v) { |component| component.clamp(min_val, max_val) }
77
78
  end
@@ -83,8 +84,10 @@ module RBGL
83
84
 
84
85
  def smoothstep(edge0, edge1, x)
85
86
  component_ternary_map(edge0, edge1, x) do |e0, e1, value|
86
- t = ((value - e0) / (e1 - e0)).clamp(0.0, 1.0)
87
- t * t * (3.0 - 2.0 * t)
87
+ next value < e0 ? 0.0 : 1.0 if e0 == e1
88
+
89
+ t = (value - e0).fdiv(e1 - e0).clamp(0.0, 1.0)
90
+ t * t * (3.0 - (2.0 * t))
88
91
  end
89
92
  end
90
93
 
@@ -97,7 +100,7 @@ module RBGL
97
100
  end
98
101
 
99
102
  def mod(x, y)
100
- component_binary_map(x, y) { |left, right| left - right * (left / right).floor }
103
+ component_binary_map(x, y) { |left, right| left - (right * (left / right).floor) }
101
104
  end
102
105
 
103
106
  def abs(x)
@@ -5,21 +5,23 @@ module RBGL
5
5
  class DynamicData
6
6
  def initialize(data = {})
7
7
  @data = {}
8
+ @writer_keys = {}
8
9
  data.to_h.each { |key, value| self[key] = value }
9
10
  end
10
11
 
11
12
  def method_missing(name, *args)
12
- if writer_method?(name)
13
- self[writer_key(name)] = args.first
14
- elsif args.empty? && @data.key?(normalize_key(name))
15
- self[name]
13
+ key = normalize_key(name)
14
+ if args.empty? && @data.key?(key)
15
+ @data[key]
16
+ elsif (writer = writer_key(name))
17
+ self[writer] = args.first
16
18
  else
17
19
  super
18
20
  end
19
21
  end
20
22
 
21
23
  def respond_to_missing?(name, include_private = false)
22
- writer_method?(name) || @data.key?(normalize_key(name)) || super
24
+ @data.key?(normalize_key(name)) || !writer_key(name).nil? || super
23
25
  end
24
26
 
25
27
  def [](key)
@@ -38,18 +40,24 @@ module RBGL
38
40
  @data.keys
39
41
  end
40
42
 
43
+ def clear
44
+ @data.clear
45
+ self
46
+ end
47
+
41
48
  private
42
49
 
43
50
  def normalize_key(key)
44
- key.to_s.chomp("=").to_sym
45
- end
51
+ return key if key.is_a?(Symbol)
46
52
 
47
- def writer_method?(name)
48
- name.to_s.end_with?("=")
53
+ key.to_s.chomp("=").to_sym
49
54
  end
50
55
 
51
56
  def writer_key(name)
52
- name.to_s.chomp("=").to_sym
57
+ return @writer_keys[name] if @writer_keys.key?(name)
58
+
59
+ text = name.to_s
60
+ @writer_keys[name] = text.end_with?("=") ? text.chomp("=").to_sym : nil
53
61
  end
54
62
  end
55
63
 
@@ -7,7 +7,7 @@ module RBGL
7
7
  WRAP_MODES = %i[repeat clamp mirror].freeze
8
8
  FILTER_MODES = %i[nearest linear].freeze
9
9
 
10
- attr_reader :width, :height, :data, :wrap_s, :wrap_t, :filter_min, :filter_mag
10
+ attr_reader :width, :height, :wrap_s, :wrap_t, :filter_min, :filter_mag
11
11
 
12
12
  WRAP_REPEAT = :repeat
13
13
  WRAP_CLAMP = :clamp
@@ -19,7 +19,7 @@ module RBGL
19
19
  def initialize(width, height, data = nil)
20
20
  @width = width
21
21
  @height = height
22
- @data = data ? normalize_data!(data) : Array.new(width * height) { Larb::Color.black }
22
+ @data = data ? normalize_data!(data) : Array.new(width * height, immutable_color(Larb::Color.black))
23
23
  @levels = [@data]
24
24
  @mipmaps_dirty = true
25
25
  @wrap_s = WRAP_REPEAT
@@ -29,16 +29,20 @@ module RBGL
29
29
  end
30
30
 
31
31
  def sample(u, v, lod: 0)
32
- u = wrap_coord(u, @wrap_s)
33
- v = wrap_coord(v, @wrap_t)
34
32
  level = mip_level_for(lod)
35
33
  level_width = width_for_level(level)
36
34
  level_height = height_for_level(level)
35
+ filter = filter_for_lod(lod)
36
+ texel_offset = filter == FILTER_LINEAR ? -0.5 : 0.0
37
37
 
38
- x = u * (level_width - 1)
39
- y = v * (level_height - 1)
38
+ x = (u.to_f * level_width) + texel_offset
39
+ y = (v.to_f * level_height) + texel_offset
40
40
 
41
- sample_with_filter(level, x, y, filter_for_lod(lod))
41
+ sample_with_filter(level, x, y, filter)
42
+ end
43
+
44
+ def data
45
+ @data.dup.freeze
42
46
  end
43
47
 
44
48
  def get_pixel(x, y)
@@ -48,7 +52,7 @@ module RBGL
48
52
  def set_pixel(x, y, color)
49
53
  return if x < 0 || x >= @width || y < 0 || y >= @height
50
54
 
51
- @data[y.to_i * @width + x.to_i] = validate_color!(color)
55
+ @data[(y.to_i * @width) + x.to_i] = immutable_color(color)
52
56
  @mipmaps_dirty = true
53
57
  end
54
58
 
@@ -86,14 +90,15 @@ module RBGL
86
90
  height.times do |y|
87
91
  width.times do |x|
88
92
  checker = ((x / size) + (y / size)) % 2
89
- data[y * width + x] = checker == 0 ? color1 : color2
93
+ data[(y * width) + x] = checker == 0 ? color1 : color2
90
94
  end
91
95
  end
92
96
  new(width, height, data)
93
97
  end
94
98
 
95
99
  def self.solid(width, height, color)
96
- new(width, height, Array.new(width * height) { color })
100
+ immutable = ImmutableColor.from(color)
101
+ new(width, height, Array.new(width * height, immutable))
97
102
  end
98
103
 
99
104
  def self.parse_ppm_header(content)
@@ -112,7 +117,7 @@ module RBGL
112
117
  def self.parse_ppm_samples(content, format, pixel_count, max_val, body_offset)
113
118
  case format
114
119
  when "P3"
115
- parse_p3_samples(content.byteslice(body_offset..), pixel_count)
120
+ parse_p3_samples(content.byteslice(body_offset..), pixel_count, max_val)
116
121
  when "P6"
117
122
  parse_p6_samples(content, body_offset, pixel_count, max_val)
118
123
  else
@@ -120,12 +125,19 @@ module RBGL
120
125
  end
121
126
  end
122
127
 
123
- def self.parse_p3_samples(body, pixel_count)
124
- samples = body.to_s.gsub(/#[^\n]*/, " ").scan(/\d+/).map(&:to_i)
128
+ def self.parse_p3_samples(body, pixel_count, max_val)
129
+ tokens = body.to_s.gsub(/#[^\n]*/, " ").split
125
130
  expected_count = pixel_count * 3
126
- raise ArgumentError, "PPM pixel data is truncated" if samples.length < expected_count
131
+ raise ArgumentError, "PPM pixel data is truncated" if tokens.length < expected_count
132
+
133
+ tokens.first(expected_count).map do |token|
134
+ raise ArgumentError, "Invalid PPM sample: #{token}" unless /\A\d+\z/.match?(token)
127
135
 
128
- samples.first(expected_count)
136
+ value = token.to_i
137
+ raise ArgumentError, "PPM sample exceeds max value: #{value}" if value > max_val
138
+
139
+ value
140
+ end
129
141
  end
130
142
 
131
143
  def self.parse_p6_samples(content, body_offset, pixel_count, max_val)
@@ -162,7 +174,7 @@ module RBGL
162
174
  raise ArgumentError, "Texture data size mismatch: expected #{expected_size}, got #{normalized.size}"
163
175
  end
164
176
 
165
- normalized.map { |pixel| validate_color!(pixel) }
177
+ normalized.map { |pixel| immutable_color(pixel) }
166
178
  end
167
179
 
168
180
  def validate_color!(color)
@@ -171,16 +183,8 @@ module RBGL
171
183
  raise ArgumentError, "Texture data must contain only Larb::Color values"
172
184
  end
173
185
 
174
- def wrap_coord(coord, mode)
175
- case mode
176
- when WRAP_REPEAT
177
- coord - coord.floor
178
- when WRAP_CLAMP
179
- coord.clamp(0.0, 1.0)
180
- when WRAP_MIRROR
181
- t = coord - coord.floor
182
- coord.floor.to_i.even? ? t : 1.0 - t
183
- end
186
+ def immutable_color(color)
187
+ ImmutableColor.from(validate_color!(color))
184
188
  end
185
189
 
186
190
  def validate_wrap_mode!(mode)
@@ -198,7 +202,7 @@ module RBGL
198
202
  end
199
203
 
200
204
  def sample_nearest(level, x, y)
201
- pixel_for_level(level, x.round, y.round)
205
+ pixel_for_level(level, x.floor, y.floor, wrap_s: @wrap_s, wrap_t: @wrap_t)
202
206
  end
203
207
 
204
208
  def filter_for_lod(lod)
@@ -224,10 +228,10 @@ module RBGL
224
228
  fx = x - x0
225
229
  fy = y - y0
226
230
 
227
- c00 = pixel_for_level(level, x0, y0)
228
- c10 = pixel_for_level(level, x1, y0)
229
- c01 = pixel_for_level(level, x0, y1)
230
- c11 = pixel_for_level(level, x1, y1)
231
+ c00 = pixel_for_level(level, x0, y0, wrap_s: @wrap_s, wrap_t: @wrap_t)
232
+ c10 = pixel_for_level(level, x1, y0, wrap_s: @wrap_s, wrap_t: @wrap_t)
233
+ c01 = pixel_for_level(level, x0, y1, wrap_s: @wrap_s, wrap_t: @wrap_t)
234
+ c11 = pixel_for_level(level, x1, y1, wrap_s: @wrap_s, wrap_t: @wrap_t)
231
235
 
232
236
  c0 = c00.lerp(c10, fx)
233
237
  c1 = c01.lerp(c11, fx)
@@ -241,13 +245,25 @@ module RBGL
241
245
  [lod.floor, @levels.size - 1].min
242
246
  end
243
247
 
244
- def pixel_for_level(level, x, y)
248
+ def pixel_for_level(level, x, y, wrap_s: WRAP_CLAMP, wrap_t: WRAP_CLAMP)
245
249
  level_width = width_for_level(level)
246
250
  level_height = height_for_level(level)
247
251
  level_data = level.zero? ? @data : level_data(level)
248
- clamped_x = x.clamp(0, level_width - 1).to_i
249
- clamped_y = y.clamp(0, level_height - 1).to_i
250
- level_data[clamped_y * level_width + clamped_x]
252
+ sample_x = wrap_index(x.to_i, level_width, wrap_s)
253
+ sample_y = wrap_index(y.to_i, level_height, wrap_t)
254
+ level_data[(sample_y * level_width) + sample_x]
255
+ end
256
+
257
+ def wrap_index(index, size, mode)
258
+ case mode
259
+ when WRAP_REPEAT
260
+ index % size
261
+ when WRAP_CLAMP
262
+ index.clamp(0, size - 1)
263
+ when WRAP_MIRROR
264
+ mirrored = index % (size * 2)
265
+ mirrored < size ? mirrored : ((size * 2) - mirrored - 1)
266
+ end
251
267
  end
252
268
 
253
269
  def level_data(level)
@@ -286,28 +302,36 @@ module RBGL
286
302
  Array.new(target_width * target_height) do |index|
287
303
  x = index % target_width
288
304
  y = index / target_width
289
- average_texel_block(source_data, source_width, source_height, x * 2, y * 2)
305
+ start_x = x * source_width.fdiv(target_width)
306
+ end_x = x == target_width - 1 ? source_width.to_f : (x + 1) * source_width.fdiv(target_width)
307
+ start_y = y * source_height.fdiv(target_height)
308
+ end_y = y == target_height - 1 ? source_height.to_f : (y + 1) * source_height.fdiv(target_height)
309
+ average_texel_area(source_data, source_width, start_x, end_x, start_y, end_y)
290
310
  end
291
311
  end
292
312
 
293
- def average_texel_block(source_data, source_width, source_height, start_x, start_y)
294
- samples = []
313
+ def average_texel_area(source_data, source_width, start_x, end_x, start_y, end_y)
314
+ red = green = blue = alpha = total_weight = 0.0
295
315
 
296
- 2.times do |dy|
297
- 2.times do |dx|
298
- x = [start_x + dx, source_width - 1].min
299
- y = [start_y + dy, source_height - 1].min
300
- samples << source_data[y * source_width + x]
316
+ (start_y.floor...end_y.ceil).each do |y|
317
+ y_weight = [end_y, y + 1].min - [start_y, y].max
318
+ (start_x.floor...end_x.ceil).each do |x|
319
+ weight = ([end_x, x + 1].min - [start_x, x].max) * y_weight
320
+ color = source_data[(y * source_width) + x]
321
+ red += color.r * weight
322
+ green += color.g * weight
323
+ blue += color.b * weight
324
+ alpha += color.a * weight
325
+ total_weight += weight
301
326
  end
302
327
  end
303
328
 
304
- sample_count = samples.length.to_f
305
- Larb::Color.new(
306
- samples.sum(&:r) / sample_count,
307
- samples.sum(&:g) / sample_count,
308
- samples.sum(&:b) / sample_count,
309
- samples.sum(&:a) / sample_count
310
- )
329
+ immutable_color(Larb::Color.new(
330
+ red / total_weight,
331
+ green / total_weight,
332
+ blue / total_weight,
333
+ alpha / total_weight
334
+ ))
311
335
  end
312
336
  end
313
337
  end
data/lib/rbgl/engine.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "version"
4
+ require_relative "engine/immutable_color"
3
5
  require_relative "engine/framebuffer"
4
6
  require_relative "engine/buffer"
5
7
  require_relative "engine/shader"
@@ -11,6 +13,6 @@ require_relative "engine/context"
11
13
 
12
14
  module RBGL
13
15
  module Engine
14
- VERSION = "0.1.0"
16
+ VERSION = RBGL::VERSION
15
17
  end
16
18
  end
@@ -26,7 +26,7 @@ module RBGL
26
26
  end
27
27
 
28
28
  def poll_events_raw
29
- []
29
+ poll_events.filter_map { |event| event.to_h if event.respond_to?(:to_h) }
30
30
  end
31
31
 
32
32
  def resize(width, height)
@@ -43,15 +43,20 @@ module RBGL
43
43
  end
44
44
 
45
45
  def set_pixels(buffer, width, height)
46
- raise NotImplementedError
46
+ bytes = validate_rgba_buffer(buffer, width, height)
47
+ present(Engine::Framebuffer.from_rgba_bytes(width, height, bytes))
47
48
  end
48
49
 
49
- def metal_available?
50
- false
51
- end
50
+ protected
51
+
52
+ def validate_rgba_buffer(buffer, width, height)
53
+ bytes = String.try_convert(buffer)
54
+ raise ArgumentError, "Pixel buffer must be a String" unless bytes
55
+
56
+ expected_size = width * height * 4
57
+ return bytes if bytes.bytesize == expected_size
52
58
 
53
- def native_handle
54
- nil
59
+ raise ArgumentError, "Pixel buffer size mismatch: expected #{expected_size}, got #{bytes.bytesize}"
55
60
  end
56
61
  end
57
62
  end
@@ -43,14 +43,18 @@ module RBGL
43
43
  FileBackend.new(width, height, title, **options)
44
44
  when :x11
45
45
  require_relative "x11/backend"
46
- X11::Backend.new(width, height, title, env: env)
46
+ X11::Backend.new(width, height, title, env: env, **options)
47
47
  when :wayland
48
48
  require_relative "wayland/backend"
49
- Wayland::Backend.new(width, height, title, env: env)
49
+ Wayland::Backend.new(width, height, title, env: env, **options)
50
50
  when :cocoa
51
51
  require_relative "cocoa/backend"
52
- Cocoa::Backend.new(width, height, title, env: env)
52
+ Cocoa::Backend.new(width, height, title, env: env, **options)
53
53
  when Backend
54
+ unless options.empty?
55
+ raise ArgumentError, "Backend instances do not accept construction options: #{options.keys.join(', ')}"
56
+ end
57
+
54
58
  backend
55
59
  else
56
60
  raise BackendSelectionError, "Unknown backend: #{backend}"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "key_mapper"
4
+
3
5
  module RBGL
4
6
  module GUI
5
7
  module Cocoa
@@ -11,7 +13,7 @@ module RBGL
11
13
  end
12
14
 
13
15
  class Backend < GUI::Backend
14
- def initialize(width, height, title = "RBGL", env: ENV)
16
+ def initialize(width, height, title = "RBGL", env: ENV, resizable: false, high_dpi: false, **_options)
15
17
  @env = env
16
18
  unless METACO_AVAILABLE
17
19
  raise LoadError, "metaco gem is required for Cocoa backend. Install it with: gem install metaco"
@@ -19,13 +21,18 @@ module RBGL
19
21
 
20
22
  super(width, height, title)
21
23
  Metaco.init
22
- @handle = Metaco.window_create(width, height, title)
24
+ @handle = Metaco.window_create(width, height, title, resizable: resizable, high_dpi: high_dpi)
25
+ @framebuffer_width, @framebuffer_height = Metaco.framebuffer_size(@handle)
23
26
  end
24
27
 
25
28
  def present(framebuffer)
26
29
  return false unless @handle
27
30
 
28
- Metaco.set_pixels(@handle, framebuffer.to_rgba_bytes, framebuffer.width, framebuffer.height)
31
+ bytes = framebuffer.to_rgba_bytes
32
+ width = @framebuffer_width
33
+ height = @framebuffer_height
34
+ bytes = scale_pixels(bytes, framebuffer.width, framebuffer.height, width, height) if [width, height] != [framebuffer.width, framebuffer.height]
35
+ Metaco.set_pixels(@handle, bytes, width, height)
29
36
  Metaco.present(@handle)
30
37
  true
31
38
  end
@@ -37,14 +44,8 @@ module RBGL
37
44
  raw_events.filter_map { |event| convert_event(event) }
38
45
  end
39
46
 
40
- def poll_events_raw
41
- return [] unless @handle
42
-
43
- Metaco.poll_events(@handle)
44
- end
45
-
46
47
  def should_close?
47
- return false unless @handle
48
+ return true unless @handle
48
49
 
49
50
  Metaco.should_close?(@handle)
50
51
  end
@@ -59,10 +60,17 @@ module RBGL
59
60
  def set_pixels(buffer, width, height)
60
61
  return unless @handle
61
62
 
62
- Metaco.set_pixels(@handle, buffer, width, height)
63
+ bytes = validate_rgba_buffer(buffer, width, height)
64
+ bytes = scale_pixels(bytes, width, height, @framebuffer_width, @framebuffer_height) if [width, height] != [@framebuffer_width, @framebuffer_height]
65
+ Metaco.set_pixels(@handle, bytes, @framebuffer_width, @framebuffer_height)
63
66
  Metaco.present(@handle)
64
67
  end
65
68
 
69
+ def resize(width, height)
70
+ super
71
+ @framebuffer_width, @framebuffer_height = Metaco.framebuffer_size(@handle)
72
+ end
73
+
66
74
  def metal_available?
67
75
  return false unless @handle
68
76
 
@@ -80,21 +88,46 @@ module RBGL
80
88
 
81
89
  case type
82
90
  when :key_press
83
- Event.new(:key_press, key: raw[:key], char: raw[:char])
91
+ Event.new(:key_press, key: normalize_key(raw[:key], raw[:char]), keycode: raw[:key], char: raw[:char], modifiers: raw[:modifiers] || [])
84
92
  when :key_release
85
- Event.new(:key_release, key: raw[:key])
93
+ Event.new(:key_release, key: normalize_key(raw[:key]), keycode: raw[:key], modifiers: raw[:modifiers] || [])
86
94
  when :mouse_press
87
- Event.new(:mouse_press, x: raw[:x], y: raw[:y], button: raw[:button])
95
+ Event.new(:mouse_press, x: raw[:x], y: mouse_y(raw[:y]), button: raw[:button], modifiers: raw[:modifiers] || [])
88
96
  when :mouse_release
89
- Event.new(:mouse_release, x: raw[:x], y: raw[:y], button: raw[:button])
97
+ Event.new(:mouse_release, x: raw[:x], y: mouse_y(raw[:y]), button: raw[:button], modifiers: raw[:modifiers] || [])
90
98
  when :mouse_move
91
- Event.new(:mouse_move, x: raw[:x], y: raw[:y])
99
+ Event.new(:mouse_move, x: raw[:x], y: mouse_y(raw[:y]), modifiers: raw[:modifiers] || [])
100
+ when :scroll
101
+ Event.new(:scroll, dx: raw[:dx], dy: raw[:dy], modifiers: raw[:modifiers] || [])
102
+ when :focus, :blur
103
+ Event.new(type)
92
104
  when :resize
93
- Event.new(:resize, width: raw[:width], height: raw[:height])
105
+ Event.new(:resize, width: raw[:width], height: raw[:height],
106
+ framebuffer_width: raw[:framebuffer_width], framebuffer_height: raw[:framebuffer_height])
94
107
  else
95
108
  nil
96
109
  end
97
110
  end
111
+
112
+ def normalize_key(key, char = nil)
113
+ KeyMapper.key(key, char)
114
+ end
115
+
116
+ def mouse_y(y)
117
+ @height ? @height - 1 - y : y
118
+ end
119
+
120
+ def scale_pixels(bytes, source_width, source_height, target_width, target_height)
121
+ scaled = String.new(capacity: target_width * target_height * 4, encoding: Encoding::BINARY)
122
+ target_height.times do |y|
123
+ source_y = y * source_height / target_height
124
+ target_width.times do |x|
125
+ source_x = x * source_width / target_width
126
+ scaled << bytes.byteslice(((source_y * source_width) + source_x) * 4, 4)
127
+ end
128
+ end
129
+ scaled
130
+ end
98
131
  end
99
132
  end
100
133
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module Cocoa
6
+ module KeyMapper
7
+ KEY_CODES = {
8
+ 0 => :a, 1 => :s, 2 => :d, 3 => :f, 4 => :h, 5 => :g, 6 => :z, 7 => :x,
9
+ 8 => :c, 9 => :v, 11 => :b, 12 => :q, 13 => :w, 14 => :e, 15 => :r,
10
+ 16 => :y, 17 => :t, 18 => :one, 19 => :two, 20 => :three, 21 => :four,
11
+ 22 => :six, 23 => :five, 24 => :equal, 25 => :nine, 26 => :seven,
12
+ 27 => :minus, 28 => :eight, 29 => :zero, 30 => :right_bracket, 31 => :o,
13
+ 32 => :u, 33 => :left_bracket, 34 => :i, 35 => :p, 36 => :enter, 37 => :l,
14
+ 38 => :j, 39 => :quote, 40 => :k, 41 => :semicolon, 42 => :backslash,
15
+ 43 => :comma, 44 => :slash, 45 => :n, 46 => :m, 47 => :period, 48 => :tab,
16
+ 49 => :space, 50 => :grave, 51 => :backspace, 53 => :escape, 54 => :right_meta,
17
+ 55 => :left_meta, 56 => :left_shift, 57 => :caps_lock, 58 => :left_alt,
18
+ 59 => :left_control, 60 => :right_shift, 61 => :right_alt, 62 => :right_control,
19
+ 63 => :function, 64 => :f17, 65 => :keypad_decimal, 67 => :keypad_multiply,
20
+ 69 => :keypad_plus, 71 => :keypad_clear, 72 => :volume_up, 73 => :volume_down,
21
+ 74 => :mute, 75 => :keypad_divide, 76 => :keypad_enter, 78 => :keypad_minus,
22
+ 79 => :f18, 80 => :f19, 81 => :keypad_equal, 82 => :keypad_zero,
23
+ 83 => :keypad_one, 84 => :keypad_two, 85 => :keypad_three, 86 => :keypad_four,
24
+ 87 => :keypad_five, 88 => :keypad_six, 89 => :keypad_seven,
25
+ 91 => :keypad_eight, 92 => :keypad_nine, 96 => :f5, 97 => :f6, 98 => :f7,
26
+ 99 => :f3, 100 => :f8, 101 => :f9, 103 => :f11, 105 => :f13,
27
+ 106 => :f16, 107 => :f14, 109 => :f10, 111 => :f12, 113 => :f15,
28
+ 114 => :help, 115 => :home, 116 => :page_up, 117 => :delete,
29
+ 118 => :f4, 119 => :end, 120 => :f2, 121 => :page_down, 122 => :f1,
30
+ 123 => :left, 124 => :right, 125 => :down, 126 => :up
31
+ }.freeze
32
+
33
+ module_function
34
+
35
+ def key(code, char = nil)
36
+ return code if code.is_a?(Symbol)
37
+ return KEY_CODES.fetch(code, code) if code.is_a?(Integer)
38
+
39
+ text = char.to_s.empty? ? code.to_s : char.to_s
40
+ return :escape if text == "\e" || text.casecmp?("escape")
41
+ return text.downcase.to_sym if text.length == 1
42
+
43
+ code
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -14,8 +14,10 @@ module RBGL
14
14
  @data[key]
15
15
  end
16
16
 
17
- def method_missing(name, *args)
18
- @data.key?(name) ? @data[name] : super
17
+ def method_missing(name, *args, &block)
18
+ return @data[name] if args.empty? && block.nil? && @data.key?(name)
19
+
20
+ super
19
21
  end
20
22
 
21
23
  def respond_to_missing?(name, include_private = false)