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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +20 -2
- data/README.md +130 -74
- data/Rakefile +7 -0
- data/bench/renderer_bench.rb +283 -0
- data/docs/.nojekyll +1 -0
- data/docs/index.html +38 -0
- data/examples/array_test.rb +1 -1
- data/examples/color_test.rb +1 -1
- data/examples/cube_spinning.rb +1 -1
- data/examples/gradient.rb +1 -1
- data/examples/multi_return_test.rb +1 -1
- data/examples/plasma.rb +1 -1
- data/examples/sphere_raymarch.rb +1 -1
- data/examples/triangle_window.rb +1 -1
- data/exe/rbgl +42 -0
- data/lib/rbgl/cli/doctor.rb +155 -0
- data/lib/rbgl/engine/buffer.rb +150 -40
- data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
- data/lib/rbgl/engine/context.rb +133 -63
- data/lib/rbgl/engine/framebuffer.rb +153 -48
- data/lib/rbgl/engine/immutable_color.rb +32 -0
- data/lib/rbgl/engine/pipeline.rb +38 -7
- data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
- data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
- data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
- data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
- data/lib/rbgl/engine/rasterizer.rb +65 -171
- data/lib/rbgl/engine/shader/base_shader.rb +51 -0
- data/lib/rbgl/engine/shader/builtins.rb +257 -0
- data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
- data/lib/rbgl/engine/shader.rb +5 -318
- data/lib/rbgl/engine/texture.rb +264 -51
- data/lib/rbgl/engine.rb +4 -1
- data/lib/rbgl/gui/backend.rb +21 -34
- data/lib/rbgl/gui/backend_factory.rb +89 -0
- data/lib/rbgl/gui/cocoa/backend.rb +59 -46
- data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
- data/lib/rbgl/gui/event.rb +4 -2
- data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
- data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
- data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
- data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
- data/lib/rbgl/gui/file_backend.rb +50 -55
- data/lib/rbgl/gui/wayland/backend.rb +214 -60
- data/lib/rbgl/gui/wayland/codec.rb +50 -0
- data/lib/rbgl/gui/wayland/connection.rb +188 -231
- data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
- data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
- data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
- data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
- data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
- data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
- data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
- data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
- data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
- data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
- data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
- data/lib/rbgl/gui/window/render_loop.rb +111 -0
- data/lib/rbgl/gui/window.rb +74 -84
- data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
- data/lib/rbgl/gui/x11/backend.rb +73 -49
- data/lib/rbgl/gui/x11/connection.rb +284 -219
- data/lib/rbgl/gui/x11/event_parser.rb +71 -0
- data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
- data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
- data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
- data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
- data/lib/rbgl/gui/x11/transport.rb +81 -0
- data/lib/rbgl/gui/x11/x_authority.rb +107 -0
- data/lib/rbgl/gui.rb +3 -5
- data/lib/rbgl/version.rb +1 -1
- metadata +63 -24
- data/examples/chemical_heartbeat.rb +0 -166
- data/examples/dark_transit.rb +0 -166
- data/examples/fractured_orb.rb +0 -428
- data/examples/fractured_orb_rb.rb +0 -598
- data/examples/hexagonal_flow.rb +0 -333
- data/examples/teapot.rb +0 -362
- data/examples/teapot_mcu.rb +0 -344
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed4b8652d75f87149d87fa786b7736bec5be6cdf4be1f9ec807e874f473c55ee
|
|
4
|
+
data.tar.gz: 71e77ebb64fea763007b1ab140eceb4119e3ebec7fdca5a216c677aaa51253c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e208d53d00554a54cd18e9e566f952b6dbce80bf222e9a390571b8a6c1a7516042ae8cab702a1fb1b2e72d8b2fbeed283a4777a5902b3d7a515a9124904bde4e
|
|
7
|
+
data.tar.gz: cf698a98c5c90ccaf00aa77d12da808a6ee170be8bbc8ef8bd845a7e9930ae8eda2deffc79611866ca1870f9b6018a2940ccae01324cde46726b93e8388c5aac
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
-
|
|
1
|
+
<h1 align="center">RBGL</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">A pure Ruby software renderer with Cocoa, Wayland, X11, and headless file backends.</p>
|
|
4
4
|
|
|
5
|
-
|
|
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&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
|
-
|
|
12
|
+
[Features](#features) · [Installation](#installation) · [Quick Start](#quick-start) · [Events](#events) · [File Backend](#file-backend)
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
gem 'rbgl'
|
|
11
|
-
```
|
|
14
|
+
***
|
|
12
15
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
Add RBGL to your Gemfile:
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
```ruby
|
|
29
|
+
gem "rbgl"
|
|
30
|
+
```
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
Then run `bundle install`, or install it directly with `gem install rbgl`.
|
|
30
33
|
|
|
31
|
-
###
|
|
34
|
+
### Requirements
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
+
## Quick Start
|
|
37
41
|
|
|
38
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
fragment do |input, _uniforms, output|
|
|
68
|
+
output.color = input.color
|
|
69
|
+
end
|
|
59
70
|
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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(
|
|
85
|
+
context.bind_vertex_buffer(vertices)
|
|
79
86
|
context.draw_arrays(:triangles, 0, 3)
|
|
80
87
|
end
|
|
81
88
|
```
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
The same rendering code works in a native window by omitting the file-only options:
|
|
84
91
|
|
|
85
92
|
```ruby
|
|
86
|
-
window
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
- Linux: Wayland or X11 (based on environment)
|
|
126
|
+
## Raw RGBA pixels
|
|
104
127
|
|
|
105
|
-
|
|
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
|
-
|
|
130
|
+
```ruby
|
|
131
|
+
rgba = "\xFF\x00\x00\xFF" * (window.width * window.height)
|
|
132
|
+
window.set_pixels(rgba)
|
|
109
133
|
```
|
|
110
134
|
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
#
|
|
118
|
-
|
|
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
|
-
|
|
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 "rbgl"
|
|
25
|
+
|
|
26
|
+
window = RBGL::GUI::Window.new(
|
|
27
|
+
width: 160, height: 120, backend: :file,
|
|
28
|
+
output_dir: "frames", 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>
|
data/examples/array_test.rb
CHANGED
data/examples/color_test.rb
CHANGED
|
@@ -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 &&
|
|
55
|
+
if e[:type] == :key_press && %i[escape q].include?(e[:key])
|
|
56
56
|
running = false
|
|
57
57
|
end
|
|
58
58
|
end
|