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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ff09c0ce9d51c2c28146dbcfdb9eecbe2a9982e45a43fbb81cd8bc1de4b96fb
4
- data.tar.gz: e58db1f718d9e8a8e1fb7cac8e3acffc026eedfc18e64d427f1669e95e7674be
3
+ metadata.gz: ed4b8652d75f87149d87fa786b7736bec5be6cdf4be1f9ec807e874f473c55ee
4
+ data.tar.gz: 71e77ebb64fea763007b1ab140eceb4119e3ebec7fdca5a216c677aaa51253c1
5
5
  SHA512:
6
- metadata.gz: b6f662d84ba8b4f44063d9d09e3ce7154bea6547ea3a53ad383b06cf719edb62d0e79ae5b85046b408d1ffa7125100d80a2414dc3213c509d41d9d89cb2e0f2c
7
- data.tar.gz: a04b7ce7b7c20502af090e26fcdf0514cfe37850266bfce5a5ad27885999c3bababe0c6089e8cf2f9f31921cdf5c27c2a1e523e03a07b31f40469e048625c3bd
6
+ metadata.gz: e208d53d00554a54cd18e9e566f952b6dbce80bf222e9a390571b8a6c1a7516042ae8cab702a1fb1b2e72d8b2fbeed283a4777a5902b3d7a515a9124904bde4e
7
+ data.tar.gz: cf698a98c5c90ccaf00aa77d12da808a6ee170be8bbc8ef8bd845a7e9930ae8eda2deffc79611866ca1870f9b6018a2940ccae01324cde46726b93e8388c5aac
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.1
data/CHANGELOG.md CHANGED
@@ -1,7 +1,25 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.1 - 2026-09-25
4
+
5
+ - Add PNG output and frame-limited file rendering.
6
+ - Add a non-blocking window frame step API.
7
+ - Fixed perspective depth interpolation, shared-edge coverage, NPOT mipmaps, repeat filtering, and shader numeric edge cases.
8
+ - Fixed native window resize, close, and Wayland disconnect handling, including internal size synchronization during raw event polling.
9
+ - Tightened vertex layout, index, draw-range, and texture data mutation contracts, and skipped unnecessary clipping and expired frame deadlines.
10
+
11
+ ## 1.0.0 - 2026-04-05
12
+
13
+ - Removed the legacy `Window#on_key`, `Window#on_mouse`, and `Window#on_resize` aliases. Register handlers with `window.on(:key_press)`, `window.on(:mouse_move)`, `window.on(:resize)`, and the other event types directly.
14
+ - Removed the legacy `backend: :native` alias. Use `backend: :auto` for automatic native backend selection.
15
+ - Changed file backend binary PPM output to `format: :ppm, ppm_mode: :binary`. The old `format: :ppm_binary` option is no longer accepted.
16
+ - Added a runtime dependency on `rlsl`, so existing applications need it resolved during install and bundle steps.
17
+ - Added binary (`P6`) and text (`P3`) PPM texture loading via `RBGL::Engine::Texture.from_ppm`, and added ASCII or binary PPM output selection for the file backend.
18
+ - Added `RBGL::GUI::Window#dropped_frames` so applications can detect failed presents, including Wayland backpressure cases.
19
+ - Improved automatic native backend selection on Linux: RBGL now prefers Wayland when available, falls back to X11 when needed, and raises clearer `BackendUnavailable` errors when no display server can be used.
20
+ - Improved rendering correctness with view-frustum clipping for triangles, lines, and points, perspective-correct interpolation, point rasterization fixes, and pipeline render-state handling.
21
+ - Tightened validation across pipelines, shaders, vertex buffers, textures, sampler settings, mipmaps, and texture writes so invalid rendering inputs fail earlier with explicit errors.
4
22
 
5
23
  ## 0.1.0 - 2026-01-04
6
24
 
7
- - Initial release.
25
+ - Initial release.
data/README.md CHANGED
@@ -1,123 +1,179 @@
1
- # RBGL
1
+ <h1 align="center">RBGL</h1>
2
2
 
3
- RuBy Graphics Library - A pure Ruby graphics library with software rendering engine and cross-platform GUI support.
3
+ <p align="center">A pure Ruby software renderer with Cocoa, Wayland, X11, and headless file backends.</p>
4
4
 
5
- ## Installation
5
+ <p align="center">
6
+ <a href="https://rubygems.org/gems/rbgl"><img src="https://badge.fury.io/rb/rbgl.svg" alt="Gem Version"></a>
7
+ <a href="https://rubygems.org/gems/rbgl"><img src="https://img.shields.io/gem/dt/rbgl?label=downloads" alt="Downloads"></a>
8
+ <a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D3.1-CC342D?logo=ruby&amp;logoColor=white" alt="Ruby Version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-750014.svg" alt="License"></a>
10
+ </p>
6
11
 
7
- Add this line to your application's Gemfile:
12
+ [Features](#features) · [Installation](#installation) · [Quick Start](#quick-start) · [Events](#events) · [File Backend](#file-backend)
8
13
 
9
- ```ruby
10
- gem 'rbgl'
11
- ```
14
+ ***
12
15
 
13
- And then execute:
16
+ RBGL (Ruby Graphics Library) is a pure Ruby software renderer with Cocoa, Wayland, X11, and headless file backends.
14
17
 
15
- ```
16
- $ bundle install
17
- ```
18
+ ## Features
18
19
 
19
- Or install it yourself as:
20
+ - Ruby-defined vertex and fragment stages with typed vertex layouts.
21
+ - Software rasterization for triangles, lines, and points, with depth and alpha blending.
22
+ - Cocoa, Wayland, X11, and headless file backends.
20
23
 
21
- ```
22
- $ gem install rbgl
23
- ```
24
+ ## Installation
24
25
 
25
- ## Requirements
26
+ Add RBGL to your Gemfile:
26
27
 
27
- - Ruby 3.1.0 or higher
28
+ ```ruby
29
+ gem "rbgl"
30
+ ```
28
31
 
29
- ## Usage
32
+ Then run `bundle install`, or install it directly with `gem install rbgl`.
30
33
 
31
- ### Basic Window
34
+ ### Requirements
32
35
 
33
- ```ruby
34
- require "rbgl"
36
+ - Ruby 3.1 or newer
37
+ - macOS: the `metaco` gem for Cocoa windows
38
+ - Linux: a Wayland compositor or X11 server
35
39
 
36
- window = RBGL::GUI::Window.new(width: 800, height: 600, title: "Hello RBGL")
40
+ ## Quick Start
37
41
 
38
- window.run do |context, delta_time|
39
- context.clear(color: Larb::Color.new(0.1, 0.1, 0.1, 1.0))
40
- end
41
- ```
42
-
43
- ### Rendering with Pipeline
42
+ This complete example renders one triangle to `frames/frame_00000.ppm`. The file backend is useful for CI, image generation, and environments without a display server.
44
43
 
44
+ <!-- rbgl-doctest: file-triangle -->
45
45
  ```ruby
46
46
  require "rbgl"
47
47
 
48
- # Create a window with rendering context
49
- window = RBGL::GUI::Window.new(width: 800, height: 600, title: "Triangle")
48
+ include Larb
49
+ include RBGL::Engine
50
+
51
+ window = RBGL::GUI::Window.new(
52
+ width: 160,
53
+ height: 120,
54
+ backend: :file,
55
+ output_dir: "frames",
56
+ format: :ppm,
57
+ max_frames: 1,
58
+ target_fps: nil
59
+ )
50
60
 
51
- # Define vertex shader
52
- vertex_shader = RBGL::Engine::Shader.new do |input, uniforms|
53
- { position: input[:position], color: input[:color] }
54
- end
61
+ pipeline = Pipeline.create do
62
+ vertex do |input, _uniforms, output|
63
+ output.position = input.position.to_vec4
64
+ output.color = input.color
65
+ end
55
66
 
56
- # Define fragment shader
57
- fragment_shader = RBGL::Engine::Shader.new do |input, uniforms|
58
- input[:color]
67
+ fragment do |input, _uniforms, output|
68
+ output.color = input.color
69
+ end
59
70
  end
60
-
61
- # Create pipeline
62
- pipeline = RBGL::Engine::Pipeline.new(
63
- vertex_shader: vertex_shader,
64
- fragment_shader: fragment_shader
71
+ pipeline.cull_mode = :none
72
+
73
+ vertices = VertexBuffer.from_array(
74
+ VertexLayout.position_color,
75
+ [
76
+ { position: Vec3[0.0, 0.7, 0.0], color: Color.red },
77
+ { position: Vec3[-0.7, -0.7, 0.0], color: Color.green },
78
+ { position: Vec3[0.7, -0.7, 0.0], color: Color.blue }
79
+ ]
65
80
  )
66
81
 
67
- # Create vertex buffer
68
- vertices = [
69
- { position: Larb::Vec4.new(0.0, 0.5, 0.0, 1.0), color: Larb::Color.red },
70
- { position: Larb::Vec4.new(-0.5, -0.5, 0.0, 1.0), color: Larb::Color.green },
71
- { position: Larb::Vec4.new(0.5, -0.5, 0.0, 1.0), color: Larb::Color.blue }
72
- ]
73
- vertex_buffer = RBGL::Engine::VertexBuffer.new(vertices)
74
-
75
- window.run do |context, delta_time|
76
- context.clear
82
+ window.run do |context, _delta_time|
83
+ context.clear(color: Color.from_hex("#101827"))
77
84
  context.bind_pipeline(pipeline)
78
- context.bind_vertex_buffer(vertex_buffer)
85
+ context.bind_vertex_buffer(vertices)
79
86
  context.draw_arrays(:triangles, 0, 3)
80
87
  end
81
88
  ```
82
89
 
83
- ### Event Handling
90
+ The same rendering code works in a native window by omitting the file-only options:
84
91
 
85
92
  ```ruby
86
- window.on_key do |event|
87
- case event.key
88
- when :escape
89
- window.stop
90
- end
93
+ window = RBGL::GUI::Window.new(
94
+ width: 800,
95
+ height: 600,
96
+ title: "RBGL Triangle",
97
+ backend: :auto
98
+ )
99
+ ```
100
+
101
+ `:auto` chooses Cocoa on macOS and prefers Wayland before X11 on Linux. You can select `:cocoa`, `:wayland`, or `:x11` explicitly.
102
+
103
+ ## Events
104
+
105
+ Register portable event handlers with `Window#on`. Keyboard events use symbols on every native backend.
106
+
107
+ ```ruby
108
+ window.on(:key_press) do |event|
109
+ window.stop if %i[escape q].include?(event.key)
91
110
  end
92
111
 
93
- window.on_mouse do |event|
112
+ window.on(:mouse_move) do |event|
94
113
  puts "Mouse: #{event.x}, #{event.y}"
95
114
  end
115
+
116
+ window.on(:resize) do |event|
117
+ puts "Size: #{event.width}x#{event.height}"
118
+ end
96
119
  ```
97
120
 
98
- ### Backend Selection
121
+ Supported portable event types include `:key_press`, `:key_release`, `:mouse_press`, `:mouse_release`, `:mouse_move`, `:scroll`, `:focus`, `:blur`, `:resize`, and `:close`. Mouse coordinates use a top-left origin. On Cocoa, key and mouse events include `modifiers`; scroll events provide pixel `dx` and `dy`.
99
122
 
100
- RBGL automatically detects the appropriate backend for your platform:
123
+ `Window#poll_events_raw` returns these normalized events as hashes when a manual loop is more convenient. Resize events
124
+ update the window, context, and backend dimensions without invoking registered event handlers.
101
125
 
102
- - macOS: Cocoa (requires `metaco` gem)
103
- - Linux: Wayland or X11 (based on environment)
126
+ ## Raw RGBA pixels
104
127
 
105
- To use the Cocoa backend on macOS, install the `metaco` gem:
128
+ `Window#set_pixels` accepts exactly `width * height * 4` bytes in RGBA order and works with Cocoa, Wayland, X11, and the file backend:
106
129
 
107
- ```
108
- $ gem install metaco
130
+ ```ruby
131
+ rgba = "\xFF\x00\x00\xFF" * (window.width * window.height)
132
+ window.set_pixels(rgba)
109
133
  ```
110
134
 
111
- You can also specify a backend explicitly:
135
+ Metal compute support is an optional Cocoa capability. Check `window.metal_available?` before using `window.native_handle`; other backends return `false` and `nil` through the window facade.
112
136
 
113
- ```ruby
114
- # Use specific backend
115
- window = RBGL::GUI::Window.new(width: 800, height: 600, backend: :x11)
137
+ ## Render loop
138
+
139
+ The native render loop is capped at 60 fps by default. Set `target_fps: 30` to choose another limit or `target_fps: nil` to disable throttling. `window.fps` reports a moving one-second average, while `window.dropped_frames` counts rejected presents.
116
140
 
117
- # Use file backend for headless rendering
118
- window = RBGL::GUI::Window.new(width: 800, height: 600, backend: :file, output_path: "output.png")
141
+ `Window#run` is intentionally one-shot: it closes its backend when the loop exits or raises. Create a new `Window` instead of calling `run` twice.
142
+
143
+ ## File backend
144
+
145
+ The file backend creates `output_dir` automatically and stops after one frame by default.
146
+
147
+ ```ruby
148
+ window = RBGL::GUI::Window.new(
149
+ width: 320,
150
+ height: 240,
151
+ backend: :file,
152
+ output_dir: "renders",
153
+ format: :bmp,
154
+ max_frames: 10
155
+ )
119
156
  ```
120
157
 
158
+ Supported formats are `:ppm`, `:bmp`, and optional `:png`. For PPM, select `ppm_mode: :ascii` or `ppm_mode: :binary`. PNG output requires the `tessel` gem. Set `max_frames: nil` only when an unlimited output sequence is intentional.
159
+
160
+ ## Examples and development
161
+
162
+ - `ruby examples/triangle_basic.rb` renders a PPM without opening a window.
163
+ - `ruby examples/triangle_window.rb` opens the portable native triangle demo.
164
+ - `ruby examples/cube_spinning.rb` renders an indexed 3D-style scene.
165
+ - `bundle exec rake test` runs the test suite and README doctest.
166
+ - `bundle exec rake lint` runs RuboCop correctness checks.
167
+ - `RBGL_BENCH_ITERATIONS=10 bundle exec ruby bench/renderer_bench.rb` runs renderer benchmarks.
168
+
169
+ Use `rbgl doctor` or `rbgl doctor --json` to inspect native backend availability.
170
+
171
+ ## Contributing
172
+
173
+ Bug reports and pull requests are welcome at [rbgfx/rbgl](https://github.com/rbgfx/rbgl).
174
+
121
175
  ## License
122
176
 
123
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
177
+ RBGL and the examples distributed in this repository are available under the [MIT License](LICENSE).
178
+ The file backend writes PPM and BMP directly, and delegates PNG output to the
179
+ optional `tessel` gem when `format: :png` is selected.
data/Rakefile CHANGED
@@ -10,3 +10,10 @@ Rake::TestTask.new(:test) do |t|
10
10
  end
11
11
 
12
12
  task default: :test
13
+
14
+ desc "Run RuboCop correctness checks"
15
+ task :lint do
16
+ sh "bundle exec rubocop --cache false --lint lib rbgl.gemspec Rakefile test/integration"
17
+ end
18
+
19
+ task verify: %i[lint test]
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Minimal renderer benchmarks for RBGL core paths.
4
+ # Usage: ruby bench/renderer_bench.rb
5
+
6
+ require "benchmark"
7
+ require "rbgl"
8
+
9
+ include Larb
10
+ include RBGL::Engine
11
+
12
+ WIDTH = Integer(ENV.fetch("RBGL_BENCH_WIDTH", "320"))
13
+ HEIGHT = Integer(ENV.fetch("RBGL_BENCH_HEIGHT", "240"))
14
+ ITERATIONS = Integer(ENV.fetch("RBGL_BENCH_ITERATIONS", "10"))
15
+
16
+ def build_context
17
+ Context.new(width: WIDTH, height: HEIGHT)
18
+ end
19
+
20
+ def triangle_pipeline
21
+ Pipeline.create do
22
+ vertex do |input, _uniforms, output|
23
+ output.position = input.position.to_vec4
24
+ output.color = input.color
25
+ end
26
+
27
+ fragment do |input, _uniforms, output|
28
+ output.color = input.color
29
+ end
30
+ end.tap { |pipeline| pipeline.cull_mode = :none }
31
+ end
32
+
33
+ def depth_pipeline
34
+ Pipeline.create do
35
+ vertex do |input, _uniforms, output|
36
+ output.position = input.position.to_vec4
37
+ output.color = input.color
38
+ end
39
+
40
+ fragment do |input, _uniforms, output|
41
+ output.color = input.color
42
+ end
43
+ end.tap do |pipeline|
44
+ pipeline.cull_mode = :none
45
+ pipeline.depth_test = true
46
+ pipeline.depth_write = true
47
+ end
48
+ end
49
+
50
+ def line_pipeline
51
+ Pipeline.create do
52
+ vertex do |input, _uniforms, output|
53
+ output.position = input.position.to_vec4
54
+ output.color = input.color
55
+ end
56
+
57
+ fragment do |input, _uniforms, output|
58
+ output.color = input.color
59
+ end
60
+ end.tap { |pipeline| pipeline.cull_mode = :none }
61
+ end
62
+
63
+ def vertex_layout
64
+ VertexLayout.position_color
65
+ end
66
+
67
+ def clear_case
68
+ context = build_context
69
+ color = Color.from_hex("#123456")
70
+
71
+ Benchmark.measure do
72
+ ITERATIONS.times { context.clear(color: color) }
73
+ end
74
+ end
75
+
76
+ def triangle_case
77
+ context = build_context
78
+ context.clear(color: Color.black)
79
+
80
+ pipeline = triangle_pipeline
81
+ context.bind_pipeline(pipeline)
82
+ context.bind_vertex_buffer(triangle_vertices)
83
+
84
+ Benchmark.measure do
85
+ ITERATIONS.times { context.draw_arrays(:triangles, 0, 3) }
86
+ end
87
+ end
88
+
89
+ def line_case
90
+ context = build_context
91
+ context.clear(color: Color.black)
92
+ context.bind_pipeline(line_pipeline)
93
+ context.bind_vertex_buffer(line_vertices)
94
+
95
+ Benchmark.measure do
96
+ ITERATIONS.times { context.draw_arrays(:lines, 0, 2) }
97
+ end
98
+ end
99
+
100
+ def depth_case
101
+ context = build_context
102
+ context.clear(color: Color.black)
103
+ context.bind_pipeline(depth_pipeline)
104
+ context.bind_vertex_buffer(depth_vertices_far)
105
+ context.draw_arrays(:triangles, 0, 3)
106
+ context.bind_vertex_buffer(depth_vertices_near)
107
+
108
+ Benchmark.measure do
109
+ ITERATIONS.times do
110
+ context.draw_arrays(:triangles, 0, 3)
111
+ context.draw_arrays(:triangles, 0, 3)
112
+ end
113
+ end
114
+ end
115
+
116
+ def texture_case
117
+ context = build_context
118
+ context.clear(color: Color.black)
119
+
120
+ texture = Texture.solid(8, 8, Color.red)
121
+ pipeline = Pipeline.create do
122
+ vertex do |input, _uniforms, output|
123
+ output.position = input.position.to_vec4
124
+ output.uv = Larb::Vec2.new((input.position.x + 1.0) * 0.5, (input.position.y + 1.0) * 0.5)
125
+ end
126
+
127
+ fragment do |input, uniforms, output|
128
+ output.color = texture(uniforms.texture, input.uv)
129
+ end
130
+ end
131
+
132
+ pipeline.cull_mode = :none
133
+ context.bind_pipeline(pipeline)
134
+ context.bind_vertex_buffer(uv_vertices)
135
+ context.set_uniform(:texture, texture)
136
+
137
+ Benchmark.measure do
138
+ ITERATIONS.times { context.draw_arrays(:triangle_strip, 0, 4) }
139
+ end
140
+ end
141
+
142
+ def shader_call_overhead_case
143
+ context = build_context
144
+ pipeline = Pipeline.create do
145
+ vertex do |input, uniforms, output|
146
+ output.position = input.position.to_vec4
147
+ output.color = Color.new(
148
+ uniforms.ramp % 1.0,
149
+ (uniforms.ramp + 0.25) % 1.0,
150
+ (uniforms.ramp + 0.5) % 1.0,
151
+ 1.0
152
+ )
153
+ end
154
+
155
+ fragment do |input, _uniforms, output|
156
+ output.color = input.color
157
+ end
158
+ end
159
+
160
+ context.bind_pipeline(pipeline)
161
+ context.bind_vertex_buffer(shaded_vertices)
162
+
163
+ t = 0.0
164
+ Benchmark.measure do
165
+ ITERATIONS.times do
166
+ context.set_uniform(:ramp, Math.sin(t))
167
+ context.draw_arrays(:triangles, 0, 3)
168
+ t += 0.01
169
+ end
170
+ end
171
+ end
172
+
173
+ def gc_alloc_case
174
+ context = build_context
175
+ context.bind_pipeline(triangle_pipeline)
176
+ context.bind_vertex_buffer(triangle_vertices)
177
+
178
+ iterations = ITERATIONS
179
+ GC.start
180
+ before = GC.stat(:total_allocated_objects)
181
+
182
+ iterations.times do
183
+ context.draw_arrays(:triangles, 0, 3)
184
+ end
185
+
186
+ after = GC.stat(:total_allocated_objects)
187
+ allocations_per_op = (after - before).fdiv(iterations)
188
+ puts "GC allocation (objects/op): #{allocations_per_op.round(2)}"
189
+ end
190
+
191
+ def triangle_vertices
192
+ VertexBuffer.from_array(
193
+ vertex_layout,
194
+ [
195
+ { position: Vec3[-0.5, 0.6, 0.0], color: Color.red },
196
+ { position: Vec3[-0.7, -0.4, 0.0], color: Color.green },
197
+ { position: Vec3[0.6, -0.4, 0.0], color: Color.blue }
198
+ ]
199
+ )
200
+ end
201
+
202
+ def line_vertices
203
+ VertexBuffer.from_array(
204
+ vertex_layout,
205
+ [
206
+ { position: Vec3[-0.8, 0.0, 0.0], color: Color.red },
207
+ { position: Vec3[0.8, 0.0, 0.0], color: Color.green }
208
+ ]
209
+ )
210
+ end
211
+
212
+ def depth_vertices_far
213
+ VertexBuffer.from_array(
214
+ vertex_layout,
215
+ [
216
+ { position: Vec3[-0.8, 0.6, 0.9], color: Color.red },
217
+ { position: Vec3[-0.6, -0.6, 0.9], color: Color.red },
218
+ { position: Vec3[0.8, -0.6, 0.9], color: Color.red }
219
+ ]
220
+ )
221
+ end
222
+
223
+ def depth_vertices_near
224
+ VertexBuffer.from_array(
225
+ vertex_layout,
226
+ [
227
+ { position: Vec3[-0.2, 0.6, 0.1], color: Color.green },
228
+ { position: Vec3[-0.4, -0.6, 0.1], color: Color.green },
229
+ { position: Vec3[0.5, -0.6, 0.1], color: Color.green }
230
+ ]
231
+ )
232
+ end
233
+
234
+ def uv_vertices
235
+ VertexBuffer.from_array(
236
+ VertexLayout.new do
237
+ attribute :position, 3
238
+ attribute :uv, 2
239
+ attribute :color, 4
240
+ end,
241
+ [
242
+ { position: Vec3[-0.8, -0.8, 0], uv: Vec2[0, 1], color: Color.white },
243
+ { position: Vec3[-0.8, 0.8, 0], uv: Vec2[0, 0], color: Color.white },
244
+ { position: Vec3[0.8, -0.8, 0], uv: Vec2[1, 1], color: Color.white },
245
+ { position: Vec3[0.8, 0.8, 0], uv: Vec2[1, 0], color: Color.white }
246
+ ]
247
+ )
248
+ end
249
+
250
+ def shaded_vertices
251
+ VertexBuffer.from_array(
252
+ vertex_layout,
253
+ [
254
+ { position: Vec3[-0.5, -0.5, 0.0], color: Color.red },
255
+ { position: Vec3[0.0, 0.8, 0.0], color: Color.green },
256
+ { position: Vec3[0.5, -0.5, 0.0], color: Color.blue }
257
+ ]
258
+ )
259
+ end
260
+
261
+ def report
262
+ cases = {
263
+ "framebuffer clear" => -> { clear_case },
264
+ "triangle draw" => -> { triangle_case },
265
+ "line draw" => -> { line_case },
266
+ "depth test" => -> { depth_case },
267
+ "texture sampling" => -> { texture_case },
268
+ "shader call overhead" => -> { shader_call_overhead_case }
269
+ }
270
+
271
+ puts "Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})"
272
+ puts "Iterations: #{ITERATIONS}"
273
+
274
+ Benchmark.bm(22) do |bench|
275
+ cases.each do |name, runner|
276
+ bench.report(name) { runner.call }
277
+ end
278
+ end
279
+
280
+ gc_alloc_case
281
+ end
282
+
283
+ report
data/docs/.nojekyll ADDED
@@ -0,0 +1 @@
1
+
data/docs/index.html ADDED
@@ -0,0 +1,38 @@
1
+ <!doctype html>
2
+ <html lang="en"><head>
3
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
4
+ <meta name="theme-color" content="#020617"><meta name="description" content="Define a graphics pipeline in Ruby, rasterize it in software, and present native or headless frames.">
5
+ <meta property="og:title" content="RBGL — A graphics pipeline you can read."><meta property="og:description" content="Define a graphics pipeline in Ruby, rasterize it in software, and present native or headless frames."><meta property="og:type" content="website"><meta property="og:url" content="https://rbgfx.github.io/rbgl/">
6
+ <meta name="twitter:card" content="summary"><link rel="canonical" href="https://rbgfx.github.io/rbgl/"><title>RBGL — A graphics pipeline you can read.</title>
7
+ <style>
8
+ :root{color-scheme:dark;--bg:#020617;--panel:#0f172a;--line:#334155;--ink:#e2e8f0;--muted:#94a3b8;--a:#38bdf8;--mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--w:min(100% - 2.5rem,76rem)}
9
+ *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.65 var(--sans)}a{color:inherit;text-underline-offset:.22em}a:hover{color:var(--a)}a:focus-visible{outline:3px solid var(--a);outline-offset:4px}h1,h2,h3,p,figure,pre{margin:0}h1,h2,h3{line-height:1.08;text-wrap:balance}p{text-wrap:pretty}code,pre,.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}.wrap{width:var(--w);margin:auto}.skip{position:fixed;left:1rem;top:1rem;z-index:1;transform:translateY(-160%);padding:.7rem 1rem;background:var(--ink);color:var(--bg)}.skip:focus{transform:none}
10
+ header{height:4.75rem;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}.brand{display:flex;align-items:center;gap:.7rem;font-weight:750;text-decoration:none}.mark{width:2rem;height:2rem;display:grid;place-items:center;border:1px solid var(--a);color:var(--a);font:.85rem var(--mono)}nav{display:flex;gap:1.5rem;font-size:.9rem}nav a{text-decoration:none;color:var(--muted)}nav a:last-child{color:var(--ink)}
11
+ .hero{display:grid;grid-template-columns:.88fr 1.12fr;align-items:center;gap:clamp(2rem,6vw,6rem);padding-block:clamp(4rem,9vw,8rem)}.kicker,.visual figcaption,.num{font:.76rem/1.4 var(--mono);color:var(--a)}h1{max-width:11ch;margin-top:1rem;font-size:clamp(3rem,7.5vw,6.5rem);letter-spacing:-.065em}.lede{max-width:40rem;margin-top:1.5rem;color:var(--muted);font-size:1.15rem}.actions{display:flex;flex-wrap:wrap;gap:.8rem;margin-top:2rem}.button{min-height:2.8rem;padding:.65rem 1rem;border:1px solid var(--line);display:inline-flex;align-items:center;text-decoration:none;font-weight:650}.button.primary{background:var(--a);border-color:var(--a);color:var(--bg)}.install{margin-top:1.4rem;color:var(--muted);font:.86rem var(--mono)}.install code{color:var(--ink)}
12
+ .visual{border:1px solid var(--line);background:var(--panel)}.art{min-height:18rem;padding:1.25rem;display:grid;place-items:center;overflow:hidden}.art svg{width:100%;max-height:23rem}.visual figcaption{display:flex;justify-content:space-between;gap:1rem;padding:.75rem 1rem;border-top:1px solid var(--line);color:var(--muted)}.visual figcaption span:last-child{color:var(--a)}
13
+ section.info{padding-block:4.5rem;border-top:1px solid var(--line)}.section-head{display:grid;grid-template-columns:.72fr 1.28fr;gap:2rem;align-items:end;margin-bottom:2rem}h2{font-size:clamp(2rem,4vw,3.25rem);letter-spacing:-.045em}.section-head p,.feature p,.try-copy p{color:var(--muted)}.features{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--line)}.feature{padding:1.25rem 1.2rem 1.4rem 0;border-bottom:1px solid var(--line)}.feature+.feature{padding-left:1.2rem;border-left:1px solid var(--line)}.feature h3{margin-top:.8rem;font-size:1.1rem}.feature p{margin-top:.5rem;font-size:.94rem}.try{display:grid;grid-template-columns:.75fr 1.25fr;align-items:center;gap:clamp(2rem,7vw,7rem)}.try-copy p{margin-top:1rem}.try-copy a{color:var(--ink)}pre{overflow-x:auto;padding:1.25rem;border:1px solid var(--line);background:var(--panel);font-size:.82rem;line-height:1.8;tab-size:2}
14
+ footer{display:flex;justify-content:space-between;gap:1rem;padding-block:1.6rem;border-top:1px solid var(--line);color:var(--muted);font-size:.85rem}footer div{display:flex;gap:1.25rem}
15
+ @media(max-width:800px){.hero{grid-template-columns:1fr;gap:2.5rem}.hero-copy{max-width:42rem}.section-head,.try{grid-template-columns:1fr;gap:1rem}}@media(max-width:560px){:root{--w:min(100% - 1.5rem,76rem)}header{height:4.2rem}nav{gap:.8rem;font-size:.8rem}nav a:first-child{display:none}h1{font-size:clamp(3rem,15vw,5rem)}.features{grid-template-columns:1fr}.feature,.feature+.feature{padding:1.1rem 0;border-left:0}.num{display:inline-block}footer{flex-direction:column}.art{min-height:14rem}section.info{padding-block:3.5rem}pre{font-size:.75rem;padding:1rem}}
16
+ @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
17
+
18
+ </style>
19
+ </head><body style="--a:#38bdf8"><a class="skip" href="#main">Skip to content</a><div class="wrap">
20
+ <header><a class="brand" href="#top" aria-label="RBGL home"><span class="mark" aria-hidden="true">R</span><span>RBGL</span></a><nav aria-label="Primary navigation"><a href="#features">Overview</a><a href="#quick-start">Quick start</a><a href="https://github.com/rbgfx/rbgl">Source ↗</a></nav></header>
21
+ <main id="main"><section class="hero" id="top" aria-labelledby="title"><div class="hero-copy"><p class="kicker">Pure Ruby software renderer</p><h1 id="title">A graphics pipeline you can read.</h1><p class="lede">Define a graphics pipeline in Ruby, rasterize it in software, and present native or headless frames.</p><div class="actions"><a class="button primary" href="#quick-start">Get started</a><a class="button" href="https://rubygems.org/gems/rbgl">RubyGems ↗</a></div><p class="install"><span aria-hidden="true">$</span> <code>gem install rbgl</code></p></div>
22
+ <figure class="visual" aria-labelledby="cap-rbgl"><div class="art"><svg viewBox="0 0 640 360" aria-hidden="true"><rect width="640" height="360" fill="#0b1224"/><path d="M320 46L554 292H86Z" fill="#082f49" stroke="#38bdf8" stroke-width="2"/><path d="M320 46L320 292M86 292L437 169M554 292L203 169" stroke="#164e63"/><circle cx="320" cy="46" r="7" fill="#e0f2fe"/><circle cx="86" cy="292" r="7" fill="#34d399"/><circle cx="554" cy="292" r="7" fill="#fb7185"/></svg></div><figcaption id="cap-rbgl"><span>Frame 00000 · PPM</span><span>vertex → fragment → raster</span></figcaption></figure></section>
23
+ <section class="info" id="features" aria-labelledby="feat"><div class="section-head"><h2 id="feat">Keep every stage in view.</h2><p>Connect typed vertex data, Ruby shader blocks, rasterization, and portable output.</p></div><div class="features"><article class="feature"><span class="num">01</span><h3>Shape the pipeline</h3><p>Describe layouts and shader stages with Ruby blocks.</p></article><article class="feature"><span class="num">02</span><h3>Rasterize in software</h3><p>Draw triangles, lines, and points with depth and alpha blending.</p></article><article class="feature"><span class="num">03</span><h3>Present anywhere</h3><p>Use Cocoa, Wayland, X11, or headless file output.</p></article></div></section>
24
+ <section class="info try" id="quick-start" aria-labelledby="quick"><div class="try-copy"><p class="kicker">A small first step</p><h2 id="quick">Start with one example.</h2><p>Install the gem, then follow the project guide for platform requirements and the full API.</p><p><a href="https://github.com/rbgfx/rbgl#installation">Read the installation guide ↗</a></p></div><pre><code>require &quot;rbgl&quot;
25
+
26
+ window = RBGL::GUI::Window.new(
27
+ width: 160, height: 120, backend: :file,
28
+ output_dir: &quot;frames&quot;, format: :ppm,
29
+ max_frames: 1, target_fps: nil
30
+ )
31
+
32
+ # Define pipeline and vertices as shown in the guide.
33
+ window.run do |context, _delta_time|
34
+ context.bind_pipeline(pipeline)
35
+ context.bind_vertex_buffer(vertices)
36
+ context.draw_arrays(:triangles, 0, 3)
37
+ end</code></pre></section></main>
38
+ <footer><span>RBGL · Ruby 3.1+ · MIT</span><div><a href="https://github.com/rbgfx/rbgl">GitHub</a><a href="https://github.com/rbgfx/rbgl/blob/main/LICENSE">License</a></div></footer></div></body></html>
@@ -82,7 +82,7 @@ while running && !window.should_close?
82
82
  events.each do |e|
83
83
  case e[:type]
84
84
  when :key_press
85
- running = false if e[:key] == 12 || e[:key] == "q"
85
+ running = false if %i[escape q].include?(e[:key])
86
86
  end
87
87
  end
88
88
 
@@ -52,7 +52,7 @@ while running && !window.should_close?
52
52
 
53
53
  events = window.poll_events_raw
54
54
  events.each do |e|
55
- if e[:type] == :key_press && (e[:key] == 12 || e[:key] == "q")
55
+ if e[:type] == :key_press && %i[escape q].include?(e[:key])
56
56
  running = false
57
57
  end
58
58
  end