rays 0.3.12 → 0.3.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b820979b1e5f5cc65ad457a6614bf068a15f03f0351709367e007a5046b02747
4
- data.tar.gz: b80a61f404fb951e609cc449cc155450aaaf2c7d0f6a2eab39001d5bd8cdc8aa
3
+ metadata.gz: be106158650d37c835a8396c0ce079a3bc829f6fdd10fed78153f5bba6e0246d
4
+ data.tar.gz: 833c9b8cd83d1e484d8424991cffd89af1a4af0a31eead4cfe8ea52db0dacead
5
5
  SHA512:
6
- metadata.gz: 9fa68f150b09b3ac24e47d8cb36c6deb4f68047755b9d5da8daccd44b568655650e6b96b4043d3c1d148bb61fc782f0a39f01dfd0ea950e078f866c0b4d1ad8c
7
- data.tar.gz: b83ee77854124708af2d926de0f354e01263362fa385feb0d30a8ad3c88c522fda44d1537ba9df73c9b07570a8e6214300c858c44584535d68b3bc10a8cdfb02
6
+ metadata.gz: ff626e5099d975fa64f4990d3c4ee12265612dde05b56103354f1cbbbbba6653770c183b262b757d103bf3e1c31824813d1afe66b3445c33090448587c81441a
7
+ data.tar.gz: 590f20a70dc9505ab609482ce98ff7fbcc9eddbdf03271597869dfadbec55a307f775327c41ffa7f630eef5d7dc7c0c6344d78070e9c5888f2c3bdef0f1de7cf
@@ -36,23 +36,9 @@ jobs:
36
36
  echo path=$(ruby -e 'print Dir.glob("*.gem").first') >> $GITHUB_OUTPUT
37
37
 
38
38
  - name: create github release
39
- id: release
40
- uses: actions/create-release@v1
41
39
  env:
42
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43
- with:
44
- tag_name: ${{ github.ref }}
45
- release_name: ${{ github.ref }}
46
-
47
- - name: upload to github release
48
- uses: actions/upload-release-asset@v1
49
- env:
50
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51
- with:
52
- upload_url: ${{ steps.release.outputs.upload_url }}
53
- asset_path: ./${{ steps.gem.outputs.path }}
54
- asset_name: ${{ steps.gem.outputs.path }}
55
- asset_content_type: application/zip
40
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41
+ run: ruby -I.github/workflows -rutils -e 'release(*ARGV)' ./${{ steps.gem.outputs.path }}
56
42
 
57
43
  - name: upload to rubygems
58
44
  env:
data/ChangeLog.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # rays ChangeLog
2
2
 
3
3
 
4
+ ## [v0.3.13] - 2026-05-17
5
+
6
+ - Improve batch rendering: defer flush until state actually changes
7
+ - Rewrite README.md
8
+ - CI: Migrate release-gem.yml from actions/create-release to gh release create
9
+
10
+ - Fix text rendering corruption when batching is enabled
11
+
12
+
4
13
  ## [v0.3.12] - 2026-05-10
5
14
 
6
15
  - Support WebAssembly
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Rays - A Drawing Engine using OpenGL.
1
+ # Rays - A 2D drawing engine on OpenGL
2
2
 
3
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/xord/rays)
4
4
  ![License](https://img.shields.io/github/license/xord/rays)
@@ -21,18 +21,39 @@ Thanks for your support! 🙌
21
21
 
22
22
  ## 🚀 About
23
23
 
24
- **Rays** is a drawing engine that utilizes OpenGL for 2D rendering.
24
+ **Rays** is a hardware-accelerated 2D drawing engine for Ruby. It is built on OpenGL and exposes a retained-mode-friendly API: build `Polygon`, `Polyline`, `Image`, `Shader`, and `Font` objects, then paint them into an off-screen `Image` (or a window provided by [Reflex](https://github.com/xord/reflex)) through a `Painter`.
25
25
 
26
- It is designed to provide efficient graphics capabilities, making it ideal for creating complex visualizations and graphics applications.
26
+ It is the rendering layer used by [Reflex](https://github.com/xord/reflex), [Processing](https://github.com/xord/processing), [RubySketch](https://github.com/xord/rubysketch), and [Reight](https://github.com/xord/reight). Like the rest of the `xord/*` family, it is primarily developed for our own use, but it also works as a standalone drawing gem.
27
+
28
+ ## 📋 Requirements
29
+
30
+ - Ruby **3.0.0** or later
31
+ - A C++ compiler with C++20 support
32
+ - [Xot](https://rubygems.org/gems/xot) and [Rucy](https://rubygems.org/gems/rucy) (declared as runtime dependencies)
33
+ - Platform graphics backend:
34
+ - **macOS** — AppKit, OpenGL, AVFoundation (bundled with the OS)
35
+ - **iOS** — UIKit, OpenGL ES, AVFoundation (bundled with the OS)
36
+ - **Windows** — GDI32, OpenGL32, GLEW (`MINGW_PACKAGE_PREFIX-glew`)
37
+ - **Linux** — `libsdl2-dev`, `libsdl2-ttf-dev`, `libglew-dev`
38
+
39
+ The following third-party libraries are cloned from GitHub and statically linked while the native extension is being built, so you do not need to install them separately:
40
+
41
+ | Library | Role |
42
+ | ------------------------------------------------------------- | ------------------------------------------------- |
43
+ | [GLM](https://github.com/g-truc/glm) | Vector / matrix math used internally |
44
+ | [Clipper](https://github.com/skyrpex/clipper) | Polygon Boolean operations (`+`, `-`, `&`, `\|`, `^`) |
45
+ | [earcut.hpp](https://github.com/mapbox/earcut.hpp) | Polygon triangulation |
46
+ | [splines-lib](https://github.com/andrewwillmott/splines-lib) | Curve / spline math |
47
+ | [stb](https://github.com/nothings/stb) (Windows / Linux only) | Image file loading |
27
48
 
28
49
  ## 📦 Installation
29
50
 
30
51
  Add this line to your Gemfile:
31
52
  ```ruby
32
- $ gem 'rays'
53
+ gem 'rays'
33
54
  ```
34
55
 
35
- Then, install gem:
56
+ Then install:
36
57
  ```bash
37
58
  $ bundle install
38
59
  ```
@@ -42,7 +63,145 @@ Or install it directly:
42
63
  $ gem install rays
43
64
  ```
44
65
 
66
+ `require 'rays'` automatically calls `Rays.init!` and registers `Rays.fin!` at exit. Set `$RAYS_NOAUTOINIT = true` before requiring if you want to manage the lifetime yourself.
67
+
68
+ Rays needs a current OpenGL context. When used through [Reflex](https://github.com/xord/reflex), the window creates and binds a context for you. To use Rays standalone for off-screen rendering, it allocates a hidden context automatically.
69
+
70
+ ## 📚 What's Included
71
+
72
+ ### Geometry and color types
73
+
74
+ | Class | Purpose |
75
+ | -------------------- | ---------------------------------------------------------------- |
76
+ | `Rays::Point` | 2D / 3D point with arithmetic operators |
77
+ | `Rays::Bounds` | Axis-aligned rectangle (position + size) |
78
+ | `Rays::Color` | RGBA color in floating-point components |
79
+ | `Rays::ColorSpace` | Pixel format / color space descriptor (RGBA, ARGB, GRAY, ...) |
80
+ | `Rays::Matrix` | 4×4 transformation matrix |
81
+
82
+ ### Drawing primitives
83
+
84
+ | Class | Purpose |
85
+ | -------------------- | -------------------------------------------------------------------------------------------- |
86
+ | `Rays::Polyline` | A single open or closed polyline; expandable into a stroked polygon |
87
+ | `Rays::Polygon` | One or more polylines forming a closed shape; supports Boolean ops via `+`, `-`, `&`, `\|`, `^` |
88
+ | `Rays::Image` | A renderable texture with an associated `Painter` for off-screen drawing |
89
+ | `Rays::Bitmap` | CPU-side pixel buffer that can be uploaded to / downloaded from an `Image` |
90
+ | `Rays::Font` | Text rendering — created from a system font name and a size |
91
+ | `Rays::Shader` | GLSL fragment / vertex shader with `set_uniform` / `uniform` for parameters |
92
+ | `Rays::Camera` | Live camera capture (per platform) rendered into an `Image` |
93
+
94
+ ### The `Painter`
95
+
96
+ `Rays::Painter` is the immediate-mode drawing surface. Obtain one through `Image#painter`, then between `painter.paint do |p| ... end` (or `begin_paint` / `end_paint`) issue draw calls:
97
+
98
+ - **Shapes** — `point`, `line`, `rect`, `ellipse`, `curve`, `bezier`, `triangle`, `quad`, `polygon`
99
+ - **Bitmaps & text** — `image`, `text`
100
+ - **State** — `fill`, `stroke`, `background`, `stroke_width`, `stroke_cap`, `stroke_join`, `blend_mode`, `clip`, `font`, `texture`, `shader`
101
+ - **Transforms** — `translate`, `scale`, `rotate`, `set_matrix`
102
+ - **Push / pop** — `push(:state, :matrix)` / `pop`, or the block form `push(fill: ..., stroke: ...) { ... }`
103
+
104
+ `stroke_cap`, `stroke_join`, `blend_mode`, `texcoord_mode`, `texcoord_wrap` accept symbols (e.g. `:round`, `:miter`, `:multiply`).
105
+
106
+ ### Top-level helpers
107
+
108
+ - `Rays.init!` / `Rays.fin!` — explicit lifecycle (called automatically on `require`)
109
+ - `Rays::Image.load(path)` / `Rays::Image#save(path)` — image file I/O
110
+ - `Rays.renderer_info` — debug info about the current OpenGL context
111
+
112
+ ## 💡 Usage
113
+
114
+ ### Draw to an off-screen image and save it
115
+
116
+ ```ruby
117
+ require 'rays'
118
+
119
+ image = Rays::Image.new(200, 200)
120
+ image.paint do |p|
121
+ p.background 0, 0, 0
122
+ p.fill 1, 0.4, 0.1 # orange-ish
123
+ p.stroke 1, 1, 1
124
+ p.stroke_width 4
125
+
126
+ p.rect 20, 20, 160, 160, round: 16
127
+ p.ellipse 100, 100, 60, 60
128
+ end
129
+
130
+ image.save 'out.png'
131
+ ```
132
+
133
+ ### Compose a polygon from Boolean operations
134
+
135
+ ```ruby
136
+ require 'rays'
137
+
138
+ a = Rays::Polygon.rect(0, 0, 100, 100)
139
+ b = Rays::Polygon.ellipse(50, 50, 80, 80)
140
+
141
+ union = a | b # outer outline of both
142
+ difference = a - b # rectangle with circular bite
143
+ intersection = a & b # lens-shaped overlap
144
+ xor = a ^ b # everything except the overlap
145
+
146
+ Rays::Image.new(140, 140).paint {|p| p.fill 1; p.polygon difference }.save 'diff.png'
147
+ ```
148
+
149
+ (The C++ operator names map to Ruby's `|`, `-`, `&`, `^`.)
150
+
151
+ ### Use a fragment shader
152
+
153
+ ```ruby
154
+ require 'rays'
155
+
156
+ invert = Rays::Shader.new <<~GLSL
157
+ uniform sampler2D texture;
158
+ varying vec4 vTexCoord;
159
+ void main() {
160
+ vec4 c = texture2D(texture, vTexCoord.xy);
161
+ gl_FragColor = vec4(1.0 - c.rgb, c.a);
162
+ }
163
+ GLSL
164
+
165
+ src = Rays::Image.load('photo.png')
166
+ out = Rays::Image.new(src.width, src.height)
167
+ out.paint do |p|
168
+ p.shader = invert
169
+ p.image src
170
+ end
171
+ out.save 'photo-invert.png'
172
+ ```
173
+
174
+ ### Drive `push` / `pop` with attributes
175
+
176
+ ```ruby
177
+ image.paint do |p|
178
+ p.push(:state, :matrix, fill: [1, 0, 0]) do
179
+ p.translate 50, 50
180
+ p.rotate 30
181
+ p.rect -20, -20, 40, 40
182
+ end
183
+ # fill, state, and matrix are restored here
184
+ end
185
+ ```
186
+
187
+ ## 🛠️ Development
188
+
189
+ ```bash
190
+ $ rake vendor # clone the external libraries into vendor/
191
+ $ rake lib # build the native C++ library (librays)
192
+ $ rake ext # build the Ruby C extension
193
+ $ rake test # run the test suite
194
+ $ rake doc # generate RDoc from C++ sources
195
+ $ rake # default: builds the extension
196
+ ```
197
+
198
+ The drawing tests render into off-screen images and compare pixels, so they require a working OpenGL context. The `test_rays_init.rb` test must run in its own process and is listed in `TESTS_ALONE`.
199
+
200
+ In the [`xord/all`](https://github.com/xord/all) monorepo you can scope by module, e.g. `rake rays test`.
201
+
45
202
  ## 📜 License
46
203
 
47
204
  **Rays** is licensed under the MIT License.
48
205
  See the [LICENSE](./LICENSE) file for details.
206
+
207
+ The third-party libraries listed above retain their own licenses (all MIT-compatible: MIT, ISC, Boost-1.0, dual-licensed MIT/Happy-Bunny for GLM, Unlicense for splines-lib).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.12
1
+ 0.3.13
data/rays.gemspec CHANGED
@@ -25,8 +25,8 @@ Gem::Specification.new do |s|
25
25
  s.platform = Gem::Platform::RUBY
26
26
  s.required_ruby_version = '>= 3.0.0'
27
27
 
28
- s.add_dependency 'xot', '~> 0.3.12'
29
- s.add_dependency 'rucy', '~> 0.3.12'
28
+ s.add_dependency 'xot', '~> 0.3.13'
29
+ s.add_dependency 'rucy', '~> 0.3.13'
30
30
 
31
31
  s.files = `git ls-files`.split $/
32
32
  s.executables = s.files.grep(%r{^bin/}) {|f| File.basename f}
@@ -24,6 +24,11 @@ namespace Rays
24
24
  {
25
25
 
26
26
 
27
+ static const Shader INVALID_SHADER;
28
+
29
+ static const Texture INVALID_TEXTURE;
30
+
31
+
27
32
  struct Vector4 : Coord4
28
33
  {
29
34
 
@@ -140,11 +145,23 @@ namespace Rays
140
145
  struct Batcher
141
146
  {
142
147
 
148
+ GLuint cached_shader_id = 0;
149
+
150
+ GLuint cached_texture_id = 0;
151
+
143
152
  int count = 0;
144
153
 
145
- Shader shader;
154
+ BlendMode blend_mode;
155
+
156
+ TexCoordMode texcoord_mode;
146
157
 
147
- Texture texture;
158
+ TexCoordWrap texcoord_wrap;
159
+
160
+ Bounds clip;
161
+
162
+ Shader shader = INVALID_SHADER;
163
+
164
+ Texture texture = INVALID_TEXTURE;
148
165
 
149
166
  std::vector<Vector4> points;
150
167
 
@@ -158,11 +175,25 @@ namespace Rays
158
175
 
159
176
  std::vector<Coord3> texcoord_maxes;
160
177
 
161
- void clear ()
178
+ void init (const PainterState& state)
162
179
  {
163
- count = 0;
164
- shader = Shader();
165
- texture = Texture();
180
+ cached_shader_id = 0;
181
+ cached_texture_id = 0;
182
+ blend_mode = state.blend_mode;
183
+ texcoord_mode = state.texcoord_mode;
184
+ texcoord_wrap = state.texcoord_wrap;
185
+ clip = state.clip;
186
+ }
187
+
188
+ void cleanup ()
189
+ {
190
+ shader = INVALID_SHADER;
191
+ texture = INVALID_TEXTURE;
192
+ }
193
+
194
+ void clear_buffers ()
195
+ {
196
+ count = 0;
166
197
  points .clear();
167
198
  indices .clear();
168
199
  colors .clear();
@@ -226,6 +257,83 @@ namespace Rays
226
257
  buffers.clear();
227
258
  }
228
259
 
260
+ void apply_blend_mode ()
261
+ {
262
+ switch (state.blend_mode)
263
+ {
264
+ case BLEND_NORMAL:
265
+ glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
266
+ glBlendFuncSeparate(
267
+ GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);
268
+ break;
269
+
270
+ case BLEND_ADD:
271
+ glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
272
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
273
+ break;
274
+
275
+ case BLEND_SUBTRACT:
276
+ glBlendEquationSeparate(GL_FUNC_REVERSE_SUBTRACT, GL_FUNC_ADD);
277
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
278
+ break;
279
+
280
+ case BLEND_LIGHTEST:
281
+ glBlendEquationSeparate(GL_MAX, GL_FUNC_ADD);
282
+ glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE);
283
+ break;
284
+
285
+ case BLEND_DARKEST:
286
+ glBlendEquationSeparate(GL_MIN, GL_FUNC_ADD);
287
+ glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE);
288
+ break;
289
+
290
+ case BLEND_EXCLUSION:
291
+ glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
292
+ glBlendFuncSeparate(
293
+ GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_ONE, GL_ONE);
294
+ break;
295
+
296
+ case BLEND_MULTIPLY:
297
+ glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
298
+ glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ONE, GL_ONE);
299
+ break;
300
+
301
+ case BLEND_SCREEN:
302
+ glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
303
+ glBlendFuncSeparate(GL_ONE_MINUS_DST_COLOR, GL_ONE, GL_ONE, GL_ONE);
304
+ break;
305
+
306
+ case BLEND_REPLACE:
307
+ glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
308
+ glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
309
+ break;
310
+
311
+ default:
312
+ argument_error(__FILE__, __LINE__, "unknown blend mode");
313
+ break;
314
+ }
315
+ OpenGL_check_error(__FILE__, __LINE__);
316
+ }
317
+
318
+ void apply_clipping ()
319
+ {
320
+ const Bounds& clip = state.clip;
321
+ if (clip)
322
+ {
323
+ coord y = frame_buffer ? clip.y : viewport.h - (clip.y + clip.h);
324
+ glEnable(GL_SCISSOR_TEST);
325
+ glScissor(
326
+ pixel_density * clip.x,
327
+ pixel_density * y,
328
+ pixel_density * clip.width,
329
+ pixel_density * clip.height);
330
+ }
331
+ else
332
+ glDisable(GL_SCISSOR_TEST);
333
+
334
+ OpenGL_check_error(__FILE__, __LINE__);
335
+ }
336
+
229
337
  };// PainterData
230
338
 
231
339
 
@@ -235,27 +343,6 @@ namespace Rays
235
343
  return (PainterData*) painter->self.get();
236
344
  }
237
345
 
238
- void
239
- Painter_update_clip (Painter* painter)
240
- {
241
- PainterData* self = get_data(painter);
242
- const Bounds& clip = self->state.clip;
243
- if (clip)
244
- {
245
- coord y = self->frame_buffer ? clip.y : self->viewport.h - (clip.y + clip.h);
246
- glEnable(GL_SCISSOR_TEST);
247
- glScissor(
248
- self->pixel_density * clip.x,
249
- self->pixel_density * y,
250
- self->pixel_density * clip.width,
251
- self->pixel_density * clip.height);
252
- }
253
- else
254
- glDisable(GL_SCISSOR_TEST);
255
-
256
- OpenGL_check_error(__FILE__, __LINE__);
257
- }
258
-
259
346
  static void
260
347
  apply_uniform (
261
348
  const ShaderProgram& program, const char* name,
@@ -477,7 +564,7 @@ namespace Rays
477
564
  static void
478
565
  setup_texcoord_variables (
479
566
  Matrix* matrix, Point* min, Point* max,
480
- const State& state, const TextureInfo& texinfo)
567
+ const PainterState& state, const TextureInfo& texinfo)
481
568
  {
482
569
  if (!texinfo.texture) return;
483
570
 
@@ -537,7 +624,7 @@ namespace Rays
537
624
 
538
625
  const ShaderProgram* program = Shader_get_program(batcher.shader);
539
626
  if (!program || !*program)
540
- return batcher.clear();
627
+ return batcher.clear_buffers();
541
628
 
542
629
  ShaderProgram_activate(*program);
543
630
 
@@ -556,7 +643,63 @@ namespace Rays
556
643
  self->cleanup();
557
644
 
558
645
  ShaderProgram_deactivate();
559
- batcher.clear();
646
+ batcher.clear_buffers();
647
+ }
648
+
649
+ static inline GLuint
650
+ get_shader_program_id (const Shader& shader)
651
+ {
652
+ const ShaderProgram* p = Shader_get_program(shader);
653
+ return p ? p->id() : 0;
654
+ }
655
+
656
+ static void
657
+ ensure_state_and_flush_batch (
658
+ Painter* painter, const Shader& shader, const Texture& texture)
659
+ {
660
+ PainterData* self = get_data(painter);
661
+ Batcher& b = self->batcher;
662
+ const PainterState& s = self->state;
663
+ GLuint shader_id = get_shader_program_id(shader);
664
+ GLuint texture_id = Texture_get_id(texture);
665
+
666
+ bool state_changed = Xot::check_and_remove_flag(
667
+ &self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
668
+ if (
669
+ !state_changed &&
670
+ b.cached_shader_id == shader_id &&
671
+ b.cached_texture_id == texture_id)
672
+ {
673
+ return;
674
+ }
675
+
676
+ bool blend_changed = b.blend_mode != s.blend_mode;
677
+ bool clip_changed = b.clip != s.clip;
678
+ if (
679
+ b.count > 0 &&
680
+ (
681
+ blend_changed ||
682
+ clip_changed ||
683
+ b.cached_shader_id != shader_id ||
684
+ b.cached_texture_id != texture_id ||
685
+ b.texcoord_mode != s.texcoord_mode ||
686
+ b.texcoord_wrap != s.texcoord_wrap
687
+ ))
688
+ {
689
+ Painter_flush(painter);
690
+ }
691
+
692
+ if (blend_changed) self->apply_blend_mode();
693
+ if (clip_changed) self->apply_clipping();
694
+
695
+ b.cached_shader_id = shader_id;
696
+ b.cached_texture_id = texture_id;
697
+ b.blend_mode = s.blend_mode;
698
+ b.texcoord_mode = s.texcoord_mode;
699
+ b.texcoord_wrap = s.texcoord_wrap;
700
+ b.clip = s.clip;
701
+ b.shader = shader;
702
+ b.texture = texture;
560
703
  }
561
704
 
562
705
  static inline Vector4
@@ -577,16 +720,8 @@ namespace Rays
577
720
  PainterData* self = get_data(painter);
578
721
  Batcher& batcher = self->batcher;
579
722
 
580
- Texture texture = texinfo ? texinfo->texture : Texture();
581
- if (
582
- batcher.points.empty() ||
583
- batcher.shader != shader ||
584
- batcher.texture != texture)
585
- {
586
- Painter_flush(painter);
587
- batcher.shader = shader;
588
- batcher.texture = texture;
589
- }
723
+ Texture texture = texinfo ? texinfo->texture : INVALID_TEXTURE;
724
+ ensure_state_and_flush_batch(painter, shader, texture);
590
725
 
591
726
  if (++batcher.count <= 5)
592
727
  {
@@ -732,7 +867,8 @@ namespace Rays
732
867
  }
733
868
  else
734
869
  {
735
- Painter_flush(painter);
870
+ ensure_state_and_flush_batch(
871
+ painter, *shader, texinfo ? texinfo->texture : INVALID_TEXTURE);
736
872
  draw(
737
873
  self, mode, color, points, npoints, indices, nindices,
738
874
  colors, texcoords, texinfo, *shader, self->position_matrix);
@@ -781,6 +917,10 @@ namespace Rays
781
917
  {
782
918
  assert(painter && font && line && *line != '\0');
783
919
 
920
+ // exclude text rendering from batching for now;
921
+ // text_image is shared and gets overwritten by next text draw
922
+ Painter_flush(painter);
923
+
784
924
  Painter::Data* self = painter->self.get();
785
925
 
786
926
  float density = self->pixel_density;
@@ -866,15 +1006,6 @@ namespace Rays
866
1006
 
867
1007
  self->opengl_state.push();
868
1008
 
869
- //glEnable(GL_CULL_FACE);
870
-
871
- glEnable(GL_DEPTH_TEST);
872
- glDepthFunc(GL_LEQUAL);
873
- OpenGL_check_error(__FILE__, __LINE__);
874
-
875
- glEnable(GL_BLEND);
876
- set_blend_mode(self->state.blend_mode);
877
-
878
1009
  FrameBuffer& fb = self->frame_buffer;
879
1010
  if (fb)
880
1011
  {
@@ -906,9 +1037,20 @@ namespace Rays
906
1037
 
907
1038
  //self->position_matrix.translate(0.375f, 0.375f);
908
1039
 
909
- Painter_update_clip(this);
1040
+ //glEnable(GL_CULL_FACE);
1041
+
1042
+ glEnable(GL_DEPTH_TEST);
1043
+ glDepthFunc(GL_LEQUAL);
1044
+ OpenGL_check_error(__FILE__, __LINE__);
1045
+
1046
+ glEnable(GL_BLEND);
1047
+ self->apply_blend_mode();
1048
+ self->apply_clipping();
910
1049
 
911
- Xot::add_flag(&self->flags, Painter::Data::PAINTING);
1050
+ self->batcher.init(self->state);
1051
+
1052
+ Xot::remove_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
1053
+ Xot:: add_flag(&self->flags, Painter::Data::PAINTING);
912
1054
 
913
1055
  glClear(GL_DEPTH_BUFFER_BIT);
914
1056
  }
@@ -937,6 +1079,8 @@ namespace Rays
937
1079
 
938
1080
  if (self->frame_buffer)
939
1081
  FrameBuffer_unbind();
1082
+
1083
+ self->batcher.cleanup();
940
1084
  }
941
1085
 
942
1086
  void
@@ -953,68 +1097,5 @@ namespace Rays
953
1097
  OpenGL_check_error(__FILE__, __LINE__);
954
1098
  }
955
1099
 
956
- void
957
- Painter::set_blend_mode (BlendMode mode)
958
- {
959
- if (self->state.blend_mode != mode)
960
- Painter_flush(this);
961
-
962
- self->state.blend_mode = mode;
963
- switch (mode)
964
- {
965
- case BLEND_NORMAL:
966
- glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
967
- glBlendFuncSeparate(
968
- GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);
969
- break;
970
-
971
- case BLEND_ADD:
972
- glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
973
- glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
974
- break;
975
-
976
- case BLEND_SUBTRACT:
977
- glBlendEquationSeparate(GL_FUNC_REVERSE_SUBTRACT, GL_FUNC_ADD);
978
- glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
979
- break;
980
-
981
- case BLEND_LIGHTEST:
982
- glBlendEquationSeparate(GL_MAX, GL_FUNC_ADD);
983
- glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE);
984
- break;
985
-
986
- case BLEND_DARKEST:
987
- glBlendEquationSeparate(GL_MIN, GL_FUNC_ADD);
988
- glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE);
989
- break;
990
-
991
- case BLEND_EXCLUSION:
992
- glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
993
- glBlendFuncSeparate(
994
- GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_ONE, GL_ONE);
995
- break;
996
-
997
- case BLEND_MULTIPLY:
998
- glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
999
- glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ONE, GL_ONE);
1000
- break;
1001
-
1002
- case BLEND_SCREEN:
1003
- glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
1004
- glBlendFuncSeparate(GL_ONE_MINUS_DST_COLOR, GL_ONE, GL_ONE, GL_ONE);
1005
- break;
1006
-
1007
- case BLEND_REPLACE:
1008
- glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
1009
- glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
1010
- break;
1011
-
1012
- default:
1013
- argument_error(__FILE__, __LINE__, "unknown blend mode");
1014
- break;
1015
- }
1016
- OpenGL_check_error(__FILE__, __LINE__);
1017
- }
1018
-
1019
1100
 
1020
1101
  }// Rays
data/src/painter.cpp CHANGED
@@ -698,6 +698,15 @@ namespace Rays
698
698
  return height;
699
699
  }
700
700
 
701
+ void
702
+ Painter::set_blend_mode (BlendMode mode)
703
+ {
704
+ if (self->state.blend_mode == mode) return;
705
+
706
+ self->state.blend_mode = mode;
707
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
708
+ }
709
+
701
710
  BlendMode
702
711
  Painter::blend_mode () const
703
712
  {
@@ -716,10 +725,8 @@ namespace Rays
716
725
  if (bounds == self->state.clip)
717
726
  return;
718
727
 
719
- Painter_flush(this);
720
-
721
728
  self->state.clip = bounds;
722
- Painter_update_clip(this);
729
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
723
730
  }
724
731
 
725
732
  void
@@ -769,9 +776,8 @@ namespace Rays
769
776
  if (image == self->state.texture)
770
777
  return;
771
778
 
772
- Painter_flush(this);
773
-
774
779
  self->state.texture = image;
780
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
775
781
  }
776
782
 
777
783
  void
@@ -779,9 +785,8 @@ namespace Rays
779
785
  {
780
786
  if (!self->state.texture) return;
781
787
 
782
- Painter_flush(this);
783
-
784
788
  self->state.texture = Image();
789
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
785
790
  }
786
791
 
787
792
  const Image&
@@ -796,9 +801,8 @@ namespace Rays
796
801
  if (mode == self->state.texcoord_mode)
797
802
  return;
798
803
 
799
- Painter_flush(this);
800
-
801
804
  self->state.texcoord_mode = mode;
805
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
802
806
  }
803
807
 
804
808
  TexCoordMode
@@ -813,9 +817,8 @@ namespace Rays
813
817
  if (wrap == self->state.texcoord_wrap)
814
818
  return;
815
819
 
816
- Painter_flush(this);
817
-
818
820
  self->state.texcoord_wrap = wrap;
821
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
819
822
  }
820
823
 
821
824
  TexCoordWrap
@@ -830,9 +833,8 @@ namespace Rays
830
833
  if (shader == self->state.shader)
831
834
  return;
832
835
 
833
- Painter_flush(this);
834
-
835
836
  self->state.shader = shader;
837
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
836
838
  }
837
839
 
838
840
  void
@@ -840,9 +842,8 @@ namespace Rays
840
842
  {
841
843
  if (!self->state.shader) return;
842
844
 
843
- Painter_flush(this);
844
-
845
845
  self->state.shader = Shader();
846
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
846
847
  }
847
848
 
848
849
  const Shader&
@@ -863,11 +864,9 @@ namespace Rays
863
864
  if (self->state_stack.empty())
864
865
  invalid_state_error(__FILE__, __LINE__, "state stack underflow.");
865
866
 
866
- Painter_flush(this);
867
-
868
867
  self->state = self->state_stack.back();
869
868
  self->state_stack.pop_back();
870
- Painter_update_clip(this);
869
+ Xot::add_flag(&self->flags, Painter::Data::UNBATCHABLE_STATE_CHANGED);
871
870
  }
872
871
 
873
872
  void
data/src/painter.h CHANGED
@@ -59,7 +59,7 @@ namespace Rays
59
59
  };// PrimitiveMode
60
60
 
61
61
 
62
- struct State
62
+ struct PainterState
63
63
  {
64
64
 
65
65
  Color background, colors[COLOR_TYPE_MAX];
@@ -82,16 +82,16 @@ namespace Rays
82
82
 
83
83
  BlendMode blend_mode;
84
84
 
85
+ TexCoordMode texcoord_mode;
86
+
87
+ TexCoordWrap texcoord_wrap;
88
+
85
89
  Bounds clip;
86
90
 
87
91
  Font font;
88
92
 
89
93
  Image texture;
90
94
 
91
- TexCoordMode texcoord_mode;
92
-
93
- TexCoordWrap texcoord_wrap;
94
-
95
95
  Shader shader;
96
96
 
97
97
  void init ()
@@ -109,11 +109,11 @@ namespace Rays
109
109
  nsegment = 0;
110
110
  line_height = -1;
111
111
  blend_mode = BLEND_NORMAL;
112
+ texcoord_mode = TEXCOORD_IMAGE;
113
+ texcoord_wrap = TEXCOORD_CLAMP;
112
114
  clip .reset(-1);
113
115
  font = get_default_font();
114
116
  texture = Image();
115
- texcoord_mode = TEXCOORD_IMAGE;
116
- texcoord_wrap = TEXCOORD_CLAMP;
117
117
  shader = Shader();
118
118
  }
119
119
 
@@ -135,7 +135,7 @@ namespace Rays
135
135
  return colors[FILL] || colors[STROKE];
136
136
  }
137
137
 
138
- };// State
138
+ };// PainterState
139
139
 
140
140
 
141
141
  struct TextureInfo
@@ -177,7 +177,9 @@ namespace Rays
177
177
  enum Flag
178
178
  {
179
179
 
180
- PAINTING = Xot::bit(1, Painter::FLAG_LAST)
180
+ PAINTING = Xot::bit(1, Painter::FLAG_LAST),
181
+
182
+ UNBATCHABLE_STATE_CHANGED = Xot::bit(2, Painter::FLAG_LAST),
181
183
 
182
184
  };// Flag
183
185
 
@@ -187,9 +189,9 @@ namespace Rays
187
189
 
188
190
  Bounds viewport;
189
191
 
190
- State state;
192
+ PainterState state;
191
193
 
192
- std::vector<State> state_stack;
194
+ std::vector<PainterState> state_stack;
193
195
 
194
196
  Matrix position_matrix;
195
197
 
@@ -214,8 +216,6 @@ namespace Rays
214
216
  };// Painter::Data
215
217
 
216
218
 
217
- void Painter_update_clip (Painter* painter);
218
-
219
219
  void Painter_flush (Painter* painter);
220
220
 
221
221
  void Painter_draw (
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rays
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.12
4
+ version: 0.3.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - xordog
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-09 00:00:00.000000000 Z
11
+ date: 2026-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xot
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.12
19
+ version: 0.3.13
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.3.12
26
+ version: 0.3.13
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rucy
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.3.12
33
+ version: 0.3.13
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.3.12
40
+ version: 0.3.13
41
41
  description: This library helps you to develop graphics application with OpenGL.
42
42
  email: xordog@gmail.com
43
43
  executables: []