rlsl 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/CHANGELOG.md +3 -1
  4. data/README.md +101 -28
  5. data/Rakefile +7 -0
  6. data/lib/rlsl/base_translator/call_parser.rb +13 -2
  7. data/lib/rlsl/base_translator/code_scanner.rb +39 -1
  8. data/lib/rlsl/base_translator.rb +24 -2
  9. data/lib/rlsl/code_generator/math_prelude.rb +103 -10
  10. data/lib/rlsl/code_generator/ruby_wrapper_generator.rb +52 -12
  11. data/lib/rlsl/code_generator/template_context.rb +6 -3
  12. data/lib/rlsl/code_generator/uniform_struct_generator.rb +1 -1
  13. data/lib/rlsl/code_generator.rb +7 -4
  14. data/lib/rlsl/compiled_shader.rb +1 -1
  15. data/lib/rlsl/errors.rb +29 -0
  16. data/lib/rlsl/glsl/translator.rb +19 -6
  17. data/lib/rlsl/msl/shader.rb +27 -9
  18. data/lib/rlsl/msl/translator.rb +25 -4
  19. data/lib/rlsl/msl/uniform_buffer_packer.rb +11 -7
  20. data/lib/rlsl/prism/ast_visitor/control_flow_visiting.rb +59 -18
  21. data/lib/rlsl/prism/ast_visitor/definition_visiting.rb +22 -9
  22. data/lib/rlsl/prism/ast_visitor/expression_visiting.rb +18 -14
  23. data/lib/rlsl/prism/ast_visitor/scope_context.rb +4 -0
  24. data/lib/rlsl/prism/ast_visitor.rb +78 -20
  25. data/lib/rlsl/prism/builtins/function_registry.rb +41 -24
  26. data/lib/rlsl/prism/builtins/operator_rules.rb +24 -0
  27. data/lib/rlsl/prism/builtins/swizzle_rules.rb +10 -1
  28. data/lib/rlsl/prism/builtins.rb +8 -0
  29. data/lib/rlsl/prism/emitters/base_emitter/control_flow_emission.rb +51 -8
  30. data/lib/rlsl/prism/emitters/base_emitter/definition_emission.rb +42 -12
  31. data/lib/rlsl/prism/emitters/base_emitter/expression_emission.rb +20 -4
  32. data/lib/rlsl/prism/emitters/base_emitter/statement_emission.rb +7 -1
  33. data/lib/rlsl/prism/emitters/base_emitter.rb +66 -5
  34. data/lib/rlsl/prism/emitters/c_emitter.rb +103 -7
  35. data/lib/rlsl/prism/emitters/glsl_emitter.rb +48 -0
  36. data/lib/rlsl/prism/emitters/msl_emitter.rb +43 -1
  37. data/lib/rlsl/prism/emitters/target_emitter.rb +56 -9
  38. data/lib/rlsl/prism/emitters/wgsl_emitter.rb +173 -18
  39. data/lib/rlsl/prism/errors.rb +9 -0
  40. data/lib/rlsl/prism/ir/control_flow.rb +6 -4
  41. data/lib/rlsl/prism/ir/definitions.rb +17 -2
  42. data/lib/rlsl/prism/ir/expressions.rb +5 -1
  43. data/lib/rlsl/prism/ir/node.rb +1 -1
  44. data/lib/rlsl/prism/ir/traversal.rb +6 -2
  45. data/lib/rlsl/prism/mutation_analyzer.rb +30 -0
  46. data/lib/rlsl/prism/node_traversal.rb +41 -0
  47. data/lib/rlsl/prism/parameter_list.rb +46 -0
  48. data/lib/rlsl/prism/return_flow_validator.rb +73 -0
  49. data/lib/rlsl/prism/source_extractor/block_locator.rb +23 -25
  50. data/lib/rlsl/prism/source_extractor.rb +7 -6
  51. data/lib/rlsl/prism/source_unit/parser.rb +42 -32
  52. data/lib/rlsl/prism/source_unit.rb +10 -10
  53. data/lib/rlsl/prism/target_capability_validator.rb +27 -2
  54. data/lib/rlsl/prism/transpiler.rb +40 -3
  55. data/lib/rlsl/prism/type_inference/call_type_resolver.rb +28 -4
  56. data/lib/rlsl/prism/type_inference/call_validator.rb +5 -1
  57. data/lib/rlsl/prism/type_inference/collection_type_resolver.rb +45 -19
  58. data/lib/rlsl/prism/type_inference/control_flow_inferer.rb +88 -7
  59. data/lib/rlsl/prism/type_inference/definition_inferer.rb +16 -1
  60. data/lib/rlsl/prism/type_inference/expression_inferer.rb +4 -0
  61. data/lib/rlsl/prism/type_inference/field_type_resolver.rb +23 -2
  62. data/lib/rlsl/prism/type_inference/scope_stack.rb +1 -1
  63. data/lib/rlsl/prism/type_inference/type_shapes.rb +3 -3
  64. data/lib/rlsl/prism/type_inference.rb +10 -5
  65. data/lib/rlsl/shader_builder/build_service.rb +34 -7
  66. data/lib/rlsl/shader_builder/native_extension_compiler.rb +50 -24
  67. data/lib/rlsl/shader_builder/shader_definition.rb +3 -3
  68. data/lib/rlsl/shader_builder/source_resolver.rb +19 -1
  69. data/lib/rlsl/shader_builder.rb +50 -10
  70. data/lib/rlsl/shader_name.rb +18 -0
  71. data/lib/rlsl/types/type_spec.rb +3 -3
  72. data/lib/rlsl/types/value_normalizer.rb +16 -11
  73. data/lib/rlsl/types.rb +0 -4
  74. data/lib/rlsl/uniform_context.rb +18 -1
  75. data/lib/rlsl/version.rb +1 -1
  76. data/lib/rlsl/wgsl/translator.rb +29 -6
  77. data/lib/rlsl/wgsl/uniform_layout.rb +25 -0
  78. data/lib/rlsl.rb +24 -3
  79. metadata +23 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15d160bc13d36ac3efc480e1a99f20b374740d27d26a30a1eb73685883d16c67
4
- data.tar.gz: 8f6a6dc93d8956138756c4e6356fb58b62b850998445b5dad8adcc02d22d4f18
3
+ metadata.gz: f67d6c410b44bdc05706df3bb9bd923d89484a779ff379a8015ff7a4003333a4
4
+ data.tar.gz: 9231a24a3a255afb8e6512af7cd6e9fa5201e5b5bdf2fad914370f0b843c80b2
5
5
  SHA512:
6
- metadata.gz: 644a31110884bacb33734231caee01dad98086a71f5d82738e343fe14e4316938a5432b0e3c3f8c47b40f8b940f534b786d46c9a3dc56ecaad2fd8414a857be4
7
- data.tar.gz: fdd2eb0cb01d5dfbf5d485e5da40bbaa61dde2c92bd6d2dda9f3ebb1f3ff0538483c6ddaa0fe8beed09ef17207e24d846ced805a0f9f473473405c6d3c737db1
6
+ metadata.gz: e6fb3852aa2f2e8910cba85f5904e8fe1de204d07544f0289bd9fba0c68c185b848b163616e5c37f53184fedb4cb62dd456a61fe36793ce450ef40de04fd7116
7
+ data.tar.gz: c52f07944ea36ca72e95ee5ea6425d5e2b410cfee054dd9ccfd23053b9b42851248f7eb93f49bfa8f8899acc953590ba7240911dd4e9dd961752a86a30d73522
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ NewCops: disable
3
+
4
+ # These class-level profile factories intentionally remain public even though
5
+ # their source location follows the protected instance API.
6
+ Lint/IneffectiveAccessModifier:
7
+ Exclude:
8
+ - lib/rlsl/base_translator.rb
data/CHANGELOG.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.1 - 2026-09-25
4
+
5
+ - Add Metal texture bindings and tighten shader typing and target validation.
4
6
 
5
7
  ## 1.0.0 - 2026-04-05
6
8
 
data/README.md CHANGED
@@ -1,6 +1,19 @@
1
- # RLSL
1
+ <h1 align="center">RLSL</h1>
2
2
 
3
- Ruby Like Shading Language - A Ruby DSL for writing shaders that transpile to multiple GPU shader languages.
3
+ <p align="center">Ruby Like Shading Language — write shaders in Ruby and transpile them to GPU and CPU targets.</p>
4
+
5
+ <p align="center">
6
+ <a href="https://rubygems.org/gems/rlsl"><img src="https://badge.fury.io/rb/rlsl.svg" alt="Gem Version"></a>
7
+ <a href="https://rubygems.org/gems/rlsl"><img src="https://img.shields.io/gem/dt/rlsl?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>
11
+
12
+ [Features](#features) · [Installation](#installation) · [Usage](#usage) · [Texture Resources](#texture-resources) · [Supported Types](#supported-types)
13
+
14
+ ***
15
+
16
+ RLSL is a Ruby DSL for shader authoring with type inference and target-specific code generation. Its C target supports CPU rendering.
4
17
 
5
18
  ## Features
6
19
 
@@ -33,6 +46,30 @@ Or install it yourself as:
33
46
  $ gem install rlsl
34
47
  ```
35
48
 
49
+ ### Requirements
50
+
51
+ - Ruby >= 3.1.0
52
+ - [Prism](https://github.com/ruby/prism) >= 1.0.0 (for Ruby parsing)
53
+ - A working Ruby C-extension toolchain for `RLSL.define` (`make` is selected from Ruby's `RbConfig`)
54
+
55
+ The test matrix covers supported Ruby releases on Ubuntu, macOS, and Windows, and runs a dedicated compatibility job against the declared Prism 1.0.0 lower bound. Metal runtime execution is macOS-only. GLSL, WGSL, and MSL source generation is platform-independent.
56
+
57
+ ### Optional: Metal Shader Execution (macOS only)
58
+
59
+ To run Metal shaders natively, install the [metaco](https://github.com/rbgfx/metaco) gem separately:
60
+
61
+ ```bash
62
+ $ gem install metaco
63
+ ```
64
+
65
+ Or add to your Gemfile:
66
+
67
+ ```ruby
68
+ gem "metaco", platforms: :ruby, install_if: -> { RUBY_PLATFORM.include?("darwin") }
69
+ ```
70
+
71
+ MSL code generation (`RLSL.to_msl` or `RLSL.define_metal`) works without metaco. The gem is only required when calling `render_metal` at runtime.
72
+
36
73
  ## Usage
37
74
 
38
75
  ### Basic Example
@@ -82,7 +119,7 @@ end
82
119
  ### Generate MSL (Metal)
83
120
 
84
121
  ```ruby
85
- metal_shader = RLSL.define_metal(:my_shader) do
122
+ msl_code = RLSL.to_msl(:my_shader) do
86
123
  uniforms do
87
124
  float :time
88
125
  end
@@ -93,6 +130,8 @@ metal_shader = RLSL.define_metal(:my_shader) do
93
130
  end
94
131
  ```
95
132
 
133
+ `RLSL.to_glsl`, `RLSL.to_wgsl`, and `RLSL.to_msl` return source strings. `RLSL.define` compiles the C target for CPU rendering, while `RLSL.define_metal` returns an `RLSL::MSL::Shader` for optional Metal execution.
134
+
96
135
  ### Using Helper Functions
97
136
 
98
137
  ```ruby
@@ -102,8 +141,8 @@ RLSL.to_glsl(:complex_shader) do
102
141
  end
103
142
 
104
143
  functions do
105
- float :noise
106
- vec3 :get_color
144
+ define :noise, returns: :float, params: { p: :vec2 }
145
+ define :get_color, returns: :vec3, params: { uv: :vec2, t: :float }
107
146
  end
108
147
 
109
148
  helpers(:ruby) do
@@ -123,6 +162,53 @@ RLSL.to_glsl(:complex_shader) do
123
162
  end
124
163
  ```
125
164
 
165
+ Every Ruby helper function needs a complete declaration in `functions`; parameter names and order must match the Ruby method definition. This prevents unknown parameters from silently becoming floats.
166
+
167
+ ### Fragment Parameters and Explicit Source
168
+
169
+ Fragment parameters are positional. The first parameter is the fragment coordinate (`vec2`), the second is the resolution (`vec2`), and the third is the uniform object. Their Ruby names may be changed; generated target code uses the canonical names `frag_coord`, `resolution`, and `u`.
170
+
171
+ Ruby-mode blocks are captured from their source file when `fragment` or `helpers` is declared. They must come from a readable file, and multiple shader blocks must not start on the same source line. Code created by `eval`, `ruby -e`, or some REPLs has no readable source file. Once declared, later edits to the file do not change the captured shader source.
172
+
173
+ For generated code and REPL use, provide the Ruby source explicitly with `fragment_source` or `helpers_source`:
174
+
175
+ ```ruby
176
+ builder = RLSL::ShaderBuilder.new(:generated_shader)
177
+ builder.fragment_source <<~RUBY
178
+ |coordinate, size, uniforms|
179
+ uv = coordinate / size
180
+ vec3(uv.x, uv.y, uniforms.time)
181
+ RUBY
182
+ ```
183
+
184
+ A block with parameters is automatically treated as Ruby shader code. A no-argument block defaults to legacy C-source mode; use `fragment(:ruby) { vec3(1.0, 0.0, 0.0) }` to select Ruby mode explicitly. Raw helper or fragment source can be selected with `helpers(:c)` and `fragment(:c)`.
185
+
186
+ WGSL generation supports Ruby shader source only. Legacy C snippets can still be translated to C, GLSL, and MSL, but `to_wgsl` rejects them because C declarations and function syntax cannot be converted into valid WGSL by identifier rewriting alone.
187
+
188
+ Use `builder.uniform_types` to inspect declared uniform types. The block form of `uniforms` remains the declaration API.
189
+
190
+ Uniform names must be ASCII identifiers and may not use the RLSL-reserved names `resolution`, `frag_coord`, or `u`. Duplicate uniform declarations are rejected. The generated GLSL uniform block uses `std140` at binding 1.
191
+
192
+ ### Texture Resources
193
+
194
+ `sampler2D` uniforms are source-generation resources for GLSL, WGSL, and MSL. Use `texture`, `texture2D`, or `textureLod` in Ruby shader code. They are not fields in the uniform buffer:
195
+
196
+ - GLSL reserves binding 0 for output and binding 1 for the uniform block; textures begin at binding 2.
197
+ - WGSL uses group 0 bindings 0 and 1 for the uniform buffer and output. Each texture/sampler pair then uses bindings 2/3, 4/5, and so on.
198
+ - MSL uses texture 0 for output, texture 1 onward for sampled textures, and buffer 0 for uniforms. The generated shader uses an internal linear sampler.
199
+
200
+ The C renderer does not bind `sampler2D` resources; attempts to use texture
201
+ functions on C are rejected with a target-capability error. Metal runtime
202
+ rendering accepts a `textures:` hash keyed by sampler name and binds each
203
+ texture in declaration order:
204
+
205
+ ```ruby
206
+ shader.render_metal(handle, width, height, uniforms, textures: { albedo: texture })
207
+ ```
208
+
209
+ Texture-enabled source can also be emitted with `to_glsl`, `to_wgsl`, or
210
+ `to_msl` and bound by the host application.
211
+
126
212
  ## Supported Types
127
213
 
128
214
  - `bool` - Boolean (conditional logic)
@@ -136,6 +222,8 @@ end
136
222
  - `mat2` - 2x2 matrix (2D texture coordinate transformations)
137
223
  - `sampler2D` - 2D texture sampler
138
224
 
225
+ Scalar and vector uniforms (`bool`, `int`, `float`, and `vec2` through `vec4`) are supported by the CPU and Metal runtime packers. Matrix and sampler uniforms are source-generation only.
226
+
139
227
  ## Built-in Functions
140
228
 
141
229
  RLSL supports common shader functions:
@@ -150,37 +238,22 @@ RLSL supports common shader functions:
150
238
  - `PI` - 3.14159265358979323846
151
239
  - `TAU` - 6.28318530717958647692
152
240
 
153
- ## Requirements
154
-
155
- - Ruby >= 3.1.0
156
- - [Prism](https://github.com/ruby/prism) gem (for Ruby parsing)
157
-
158
- ### Optional: Metal Shader Execution (macOS only)
159
-
160
- To run Metal shaders natively, install the [metaco](https://github.com/ydah/metaco) gem separately:
161
-
162
- ```bash
163
- $ gem install metaco
164
- ```
165
-
166
- Or add to your Gemfile:
167
-
168
- ```ruby
169
- gem "metaco", platforms: :ruby, install_if: -> { RUBY_PLATFORM.include?("darwin") }
170
- ```
171
-
172
- MSL code generation (`RLSL.define_metal`) works without metaco. The gem is only required when calling `render_metal` at runtime.
241
+ Ruby-style `Math::PI` and `Math::TAU` are accepted as aliases in shader source.
173
242
 
174
243
  ## Development
175
244
 
176
- After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
245
+ After checking out the repo, run `bundle install` to install dependencies. Then run `rake verify` for lint and tests.
177
246
 
178
247
  ```bash
179
248
  $ bundle install
180
- $ rake test
249
+ $ rake verify
181
250
  ```
182
251
 
183
- For a quick local coverage summary, run `COVERAGE=1 rake test`. It prints the overall `lib/` coverage and the lowest-covered files without requiring extra gems.
252
+ For a quick local coverage summary, run `COVERAGE=1 rake test`. Set `COVERAGE_MIN=85` to enforce a minimum percentage. Target compiler integration tests use `glslangValidator`, Naga's `naga` CLI, and `xcrun metal` when those tools are installed; CI requires the GLSL and WGSL validators.
253
+
254
+ ## Contributing
255
+
256
+ Bug reports and pull requests are welcome at [rbgfx/rlsl](https://github.com/rbgfx/rlsl).
184
257
 
185
258
  ## License
186
259
 
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "rubocop/rake_task"
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
8
  t.libs << "test"
@@ -10,3 +11,9 @@ Rake::TestTask.new(:test) do |t|
10
11
  end
11
12
 
12
13
  task default: :test
14
+
15
+ RuboCop::RakeTask.new(:lint) do |task|
16
+ task.options = ["--cache", "false", "--only", "Lint,Security", "lib"]
17
+ end
18
+
19
+ task verify: %i[lint test]
@@ -12,7 +12,7 @@ module RLSL
12
12
  char = segment[index]
13
13
 
14
14
  if quote
15
- quote = nil if char == quote && segment[index - 1] != "\\"
15
+ quote = nil if char == quote && !escaped?(segment, index)
16
16
  else
17
17
  case char
18
18
  when '"', "'"
@@ -39,7 +39,7 @@ module RLSL
39
39
 
40
40
  arguments_source.each_char.with_index do |char, index|
41
41
  if quote
42
- quote = nil if char == quote && arguments_source[index - 1] != "\\"
42
+ quote = nil if char == quote && !escaped?(arguments_source, index)
43
43
  next
44
44
  end
45
45
 
@@ -63,6 +63,17 @@ module RLSL
63
63
 
64
64
  arguments << tail
65
65
  end
66
+
67
+ def self.escaped?(source, index)
68
+ backslashes = 0
69
+ cursor = index - 1
70
+ while cursor >= 0 && source[cursor] == "\\"
71
+ backslashes += 1
72
+ cursor -= 1
73
+ end
74
+
75
+ backslashes.odd?
76
+ end
66
77
  end
67
78
  end
68
79
  end
@@ -39,6 +39,11 @@ module RLSL
39
39
  private
40
40
 
41
41
  def read_preserved_segment(index)
42
+ if preprocessor_start?(index)
43
+ directive, next_index = read_preprocessor(index)
44
+ return [{ type: :preprocessor, text: directive }, next_index]
45
+ end
46
+
42
47
  if line_comment_start?(index)
43
48
  comment, next_index = read_line_comment(index)
44
49
  return [{ type: :line_comment, text: comment }, next_index]
@@ -57,6 +62,28 @@ module RLSL
57
62
  nil
58
63
  end
59
64
 
65
+ def preprocessor_start?(index)
66
+ return false unless @code[index] == "#"
67
+
68
+ line_start = @code.rindex("\n", index - 1)
69
+ prefix = @code[(line_start ? line_start + 1 : 0)...index]
70
+ prefix.match?(/\A[ \t]*\z/)
71
+ end
72
+
73
+ def read_preprocessor(index)
74
+ cursor = index
75
+
76
+ loop do
77
+ newline_index = @code.index("\n", cursor)
78
+ return [@code[index..], @code.length] unless newline_index
79
+
80
+ line = @code[cursor...newline_index]
81
+ return [@code[index...newline_index], newline_index] unless line.rstrip.end_with?("\\")
82
+
83
+ cursor = newline_index + 1
84
+ end
85
+ end
86
+
60
87
  def line_comment_start?(index)
61
88
  @code[index, 2] == "//"
62
89
  end
@@ -88,7 +115,7 @@ module RLSL
88
115
  cursor = index + 1
89
116
 
90
117
  while cursor < @code.length
91
- if @code[cursor] == quote && @code[cursor - 1] != "\\"
118
+ if @code[cursor] == quote && !escaped?(cursor)
92
119
  return [@code[index..cursor], cursor + 1]
93
120
  end
94
121
 
@@ -97,6 +124,17 @@ module RLSL
97
124
 
98
125
  raise ArgumentError, "Unterminated string literal in translation source"
99
126
  end
127
+
128
+ def escaped?(index)
129
+ backslashes = 0
130
+ cursor = index - 1
131
+ while cursor >= 0 && @code[cursor] == "\\"
132
+ backslashes += 1
133
+ cursor -= 1
134
+ end
135
+
136
+ backslashes.odd?
137
+ end
100
138
  end
101
139
  end
102
140
  end
@@ -28,8 +28,9 @@ module RLSL
28
28
 
29
29
  REMOVED_IDENTIFIERS = %w[static inline].freeze
30
30
 
31
- def initialize(uniforms, helpers_code, fragment_code)
31
+ def initialize(uniforms, helpers_code, fragment_code, name: nil)
32
32
  @uniforms = uniforms
33
+ @shader_name = name&.to_s
33
34
  @helpers_source = normalize_source(helpers_code)
34
35
  @fragment_source = normalize_source(fragment_code)
35
36
  end
@@ -45,11 +46,14 @@ module RLSL
45
46
  def translate_code(source)
46
47
  snippet = normalize_source(source)
47
48
  return "" if snippet.code.empty?
49
+ validate_source_format!(snippet)
48
50
  return snippet.code if snippet.target_code?
49
51
 
50
52
  profile.translate(snippet.code)
51
53
  end
52
54
 
55
+ def validate_source_format!(_source); end
56
+
53
57
  def generate_shader(_helpers, _fragment)
54
58
  raise NotImplementedError, "Subclasses must implement generate_shader"
55
59
  end
@@ -117,12 +121,30 @@ module RLSL
117
121
 
118
122
  def uniform_lines(resolution_line:, &block)
119
123
  lines = [resolution_line]
120
- @uniforms.each do |name, type|
124
+ value_uniforms.each do |name, type|
121
125
  lines << block.call(name, uniform_type_to_target(type))
122
126
  end
123
127
  lines
124
128
  end
125
129
 
130
+ def value_uniforms
131
+ @uniforms.reject { |_name, type| type.to_sym == :sampler2D }
132
+ end
133
+
134
+ def texture_uniforms
135
+ @uniforms.select { |_name, type| type.to_sym == :sampler2D }
136
+ end
137
+
138
+ def indent_source(source, spaces)
139
+ prefix = " " * spaces
140
+ source.to_s.lines.map { |line| line.strip.empty? ? line : "#{prefix}#{line}" }.join.rstrip
141
+ end
142
+
143
+ def generated_by_comment(language)
144
+ suffix = @shader_name ? ": #{@shader_name}" : ""
145
+ "// #{language} shader generated by RLSL#{suffix}"
146
+ end
147
+
126
148
  def profile
127
149
  return self.class::PROFILE if self.class.const_defined?(:PROFILE, false)
128
150
 
@@ -10,33 +10,125 @@ module RLSL
10
10
  static inline vec2 vec2_new(float x, float y) { return (vec2){x, y}; }
11
11
  static inline vec3 vec3_new(float x, float y, float z) { return (vec3){x, y, z}; }
12
12
  static inline vec4 vec4_new(float x, float y, float z, float w) { return (vec4){x, y, z, w}; }
13
+ static inline vec2 vec2_splat(float x) { return vec2_new(x, x); }
14
+ static inline vec3 vec3_splat(float x) { return vec3_new(x, x, x); }
15
+ static inline vec4 vec4_splat(float x) { return vec4_new(x, x, x, x); }
16
+
17
+ static inline vec2 vec2_from_vec2(vec2 v) { return v; }
18
+ static inline vec3 vec3_from_vec3(vec3 v) { return v; }
19
+ static inline vec4 vec4_from_vec4(vec4 v) { return v; }
20
+ static inline vec3 vec3_from_vec2_float(vec2 v, float z) { return vec3_new(v.x, v.y, z); }
21
+ static inline vec3 vec3_from_float_vec2(float x, vec2 v) { return vec3_new(x, v.x, v.y); }
22
+ static inline vec4 vec4_from_vec3_float(vec3 v, float w) { return vec4_new(v.x, v.y, v.z, w); }
23
+ static inline vec4 vec4_from_float_vec3(float x, vec3 v) { return vec4_new(x, v.x, v.y, v.z); }
24
+ static inline vec4 vec4_from_vec2_vec2(vec2 a, vec2 b) { return vec4_new(a.x, a.y, b.x, b.y); }
25
+ static inline vec4 vec4_from_vec2_float_float(vec2 v, float z, float w) { return vec4_new(v.x, v.y, z, w); }
26
+ static inline vec4 vec4_from_float_vec2_float(float x, vec2 v, float w) { return vec4_new(x, v.x, v.y, w); }
27
+ static inline vec4 vec4_from_float_float_vec2(float x, float y, vec2 v) { return vec4_new(x, y, v.x, v.y); }
28
+
29
+ static inline float vec2_component(vec2 v, int i) { return i == 0 ? v.x : v.y; }
30
+ static inline float vec3_component(vec3 v, int i) { return i == 0 ? v.x : (i == 1 ? v.y : v.z); }
31
+ static inline float vec4_component(vec4 v, int i) {
32
+ return i == 0 ? v.x : (i == 1 ? v.y : (i == 2 ? v.z : v.w));
33
+ }
34
+
35
+ #define RLSL_DEFINE_SWIZZLES(type) \
36
+ static inline vec2 type##_swizzle2(type v, int a, int b) { \
37
+ return vec2_new(type##_component(v, a), type##_component(v, b)); \
38
+ } \
39
+ static inline vec3 type##_swizzle3(type v, int a, int b, int c) { \
40
+ return vec3_new(type##_component(v, a), type##_component(v, b), type##_component(v, c)); \
41
+ } \
42
+ static inline vec4 type##_swizzle4(type v, int a, int b, int c, int d) { \
43
+ return vec4_new(type##_component(v, a), type##_component(v, b), type##_component(v, c), type##_component(v, d)); \
44
+ }
45
+ RLSL_DEFINE_SWIZZLES(vec2)
46
+ RLSL_DEFINE_SWIZZLES(vec3)
47
+ RLSL_DEFINE_SWIZZLES(vec4)
48
+ #undef RLSL_DEFINE_SWIZZLES
13
49
 
14
50
  static inline vec2 vec2_add(vec2 a, vec2 b) { return (vec2){a.x + b.x, a.y + b.y}; }
15
51
  static inline vec3 vec3_add(vec3 a, vec3 b) { return (vec3){a.x + b.x, a.y + b.y, a.z + b.z}; }
52
+ static inline vec4 vec4_add(vec4 a, vec4 b) { return (vec4){a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w}; }
16
53
 
17
54
  static inline vec2 vec2_sub(vec2 a, vec2 b) { return (vec2){a.x - b.x, a.y - b.y}; }
18
55
  static inline vec3 vec3_sub(vec3 a, vec3 b) { return (vec3){a.x - b.x, a.y - b.y, a.z - b.z}; }
19
-
20
- static inline vec2 vec2_mul(vec2 a, float s) { return (vec2){a.x * s, a.y * s}; }
21
- static inline vec3 vec3_mul(vec3 a, float s) { return (vec3){a.x * s, a.y * s, a.z * s}; }
22
-
23
- static inline vec2 vec2_div(vec2 a, float s) { return (vec2){a.x / s, a.y / s}; }
24
- static inline vec3 vec3_div(vec3 a, float s) { return (vec3){a.x / s, a.y / s, a.z / s}; }
56
+ static inline vec4 vec4_sub(vec4 a, vec4 b) { return (vec4){a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w}; }
57
+
58
+ static inline vec2 vec2_add_scalar(vec2 a, float s) { return (vec2){a.x + s, a.y + s}; }
59
+ static inline vec3 vec3_add_scalar(vec3 a, float s) { return (vec3){a.x + s, a.y + s, a.z + s}; }
60
+ static inline vec4 vec4_add_scalar(vec4 a, float s) { return (vec4){a.x + s, a.y + s, a.z + s, a.w + s}; }
61
+ static inline vec2 vec2_scalar_add(float s, vec2 a) { return vec2_add_scalar(a, s); }
62
+ static inline vec3 vec3_scalar_add(float s, vec3 a) { return vec3_add_scalar(a, s); }
63
+ static inline vec4 vec4_scalar_add(float s, vec4 a) { return vec4_add_scalar(a, s); }
64
+
65
+ static inline vec2 vec2_sub_scalar(vec2 a, float s) { return (vec2){a.x - s, a.y - s}; }
66
+ static inline vec3 vec3_sub_scalar(vec3 a, float s) { return (vec3){a.x - s, a.y - s, a.z - s}; }
67
+ static inline vec4 vec4_sub_scalar(vec4 a, float s) { return (vec4){a.x - s, a.y - s, a.z - s, a.w - s}; }
68
+ static inline vec2 vec2_scalar_sub(float s, vec2 a) { return (vec2){s - a.x, s - a.y}; }
69
+ static inline vec3 vec3_scalar_sub(float s, vec3 a) { return (vec3){s - a.x, s - a.y, s - a.z}; }
70
+ static inline vec4 vec4_scalar_sub(float s, vec4 a) { return (vec4){s - a.x, s - a.y, s - a.z, s - a.w}; }
71
+
72
+ static inline vec2 vec2_mul_scalar(vec2 a, float s) { return (vec2){a.x * s, a.y * s}; }
73
+ static inline vec3 vec3_mul_scalar(vec3 a, float s) { return (vec3){a.x * s, a.y * s, a.z * s}; }
74
+ static inline vec4 vec4_mul_scalar(vec4 a, float s) { return (vec4){a.x * s, a.y * s, a.z * s, a.w * s}; }
75
+ static inline vec2 vec2_scalar_mul(float s, vec2 a) { return vec2_mul_scalar(a, s); }
76
+ static inline vec3 vec3_scalar_mul(float s, vec3 a) { return vec3_mul_scalar(a, s); }
77
+ static inline vec4 vec4_scalar_mul(float s, vec4 a) { return vec4_mul_scalar(a, s); }
78
+ static inline vec2 vec2_mul_components(vec2 a, vec2 b) { return (vec2){a.x * b.x, a.y * b.y}; }
79
+ static inline vec3 vec3_mul_components(vec3 a, vec3 b) { return (vec3){a.x * b.x, a.y * b.y, a.z * b.z}; }
80
+ static inline vec4 vec4_mul_components(vec4 a, vec4 b) { return (vec4){a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w}; }
81
+
82
+ static inline vec2 vec2_div_scalar(vec2 a, float s) { return (vec2){a.x / s, a.y / s}; }
83
+ static inline vec3 vec3_div_scalar(vec3 a, float s) { return (vec3){a.x / s, a.y / s, a.z / s}; }
84
+ static inline vec4 vec4_div_scalar(vec4 a, float s) { return (vec4){a.x / s, a.y / s, a.z / s, a.w / s}; }
85
+ static inline vec2 vec2_scalar_div(float s, vec2 a) { return (vec2){s / a.x, s / a.y}; }
86
+ static inline vec3 vec3_scalar_div(float s, vec3 a) { return (vec3){s / a.x, s / a.y, s / a.z}; }
87
+ static inline vec4 vec4_scalar_div(float s, vec4 a) { return (vec4){s / a.x, s / a.y, s / a.z, s / a.w}; }
88
+ static inline vec2 vec2_div_components(vec2 a, vec2 b) { return (vec2){a.x / b.x, a.y / b.y}; }
89
+ static inline vec3 vec3_div_components(vec3 a, vec3 b) { return (vec3){a.x / b.x, a.y / b.y, a.z / b.z}; }
90
+ static inline vec4 vec4_div_components(vec4 a, vec4 b) { return (vec4){a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w}; }
91
+
92
+ /* Legacy names used by raw C fragments. */
93
+ static inline vec2 vec2_mul(vec2 a, float s) { return vec2_mul_scalar(a, s); }
94
+ static inline vec3 vec3_mul(vec3 a, float s) { return vec3_mul_scalar(a, s); }
95
+ static inline vec2 vec2_div(vec2 a, float s) { return vec2_div_scalar(a, s); }
96
+ static inline vec3 vec3_div(vec3 a, float s) { return vec3_div_scalar(a, s); }
25
97
 
26
98
  static inline float vec2_dot(vec2 a, vec2 b) { return a.x * b.x + a.y * b.y; }
27
99
  static inline float vec3_dot(vec3 a, vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
100
+ static inline float vec4_dot(vec4 a, vec4 b) { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; }
101
+
102
+ static inline vec3 vec3_cross(vec3 a, vec3 b) {
103
+ return vec3_new(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
104
+ }
28
105
 
29
106
  static inline float vec2_length(vec2 v) { return sqrtf(v.x * v.x + v.y * v.y); }
30
107
  static inline float vec3_length(vec3 v) { return sqrtf(v.x * v.x + v.y * v.y + v.z * v.z); }
108
+ static inline float vec4_length(vec4 v) { return sqrtf(vec4_dot(v, v)); }
109
+
110
+ static inline float vec2_distance(vec2 a, vec2 b) { return vec2_length(vec2_sub(a, b)); }
111
+ static inline float vec3_distance(vec3 a, vec3 b) { return vec3_length(vec3_sub(a, b)); }
112
+ static inline float vec4_distance(vec4 a, vec4 b) { return vec4_length(vec4_sub(a, b)); }
31
113
 
32
- static inline vec2 vec2_normalize(vec2 v) { float l = vec2_length(v); return l > 0 ? vec2_div(v, l) : v; }
33
- static inline vec3 vec3_normalize(vec3 v) { float l = vec3_length(v); return l > 0 ? vec3_div(v, l) : v; }
114
+ static inline vec2 vec2_normalize(vec2 v) { float l = vec2_length(v); return l > 0 ? vec2_div_scalar(v, l) : v; }
115
+ static inline vec3 vec3_normalize(vec3 v) { float l = vec3_length(v); return l > 0 ? vec3_div_scalar(v, l) : v; }
116
+ static inline vec4 vec4_normalize(vec4 v) { float l = vec4_length(v); return l > 0 ? vec4_div_scalar(v, l) : v; }
34
117
 
35
118
  static inline float fract(float x) { return x - floorf(x); }
119
+ static inline float rlsl_mod(float x, float y) { return x - y * floorf(x / y); }
120
+ static inline float sign_f(float x) { return (x > 0.0f) - (x < 0.0f); }
121
+ static inline float step_f(float edge, float x) { return x < edge ? 0.0f : 1.0f; }
36
122
  static inline float mix_f(float a, float b, float t) { return a + (b - a) * t; }
123
+ static inline vec2 mix_v2(vec2 a, vec2 b, float t) {
124
+ return (vec2){a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t};
125
+ }
37
126
  static inline vec3 mix_v3(vec3 a, vec3 b, float t) {
38
127
  return (vec3){a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t};
39
128
  }
129
+ static inline vec4 mix_v4(vec4 a, vec4 b, float t) {
130
+ return (vec4){a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t};
131
+ }
40
132
 
41
133
  static inline float clamp_f(float x, float lo, float hi) {
42
134
  return x < lo ? lo : (x > hi ? hi : x);
@@ -53,8 +145,9 @@ module RLSL
53
145
  }
54
146
 
55
147
  static inline vec2 hash22(vec2 p) {
56
- float n = sinf(vec2_dot(p, vec2_new(127.1f, 311.7f)));
57
- return vec2_new(fract(n * 43758.5453f), fract(n * 12345.6789f));
148
+ float x = sinf(vec2_dot(p, vec2_new(127.1f, 311.7f)));
149
+ float y = sinf(vec2_dot(p, vec2_new(269.5f, 183.3f)));
150
+ return vec2_new(fract(x * 43758.5453f), fract(y * 43758.5453f));
58
151
  }
59
152
 
60
153
  static inline vec3 reflect(vec3 I, vec3 N) {
@@ -20,7 +20,7 @@ module RLSL
20
20
  for (int x = 0; x < width; x++) {
21
21
  vec2 frag_coord = vec2_new((float)x, (float)flipped_y);
22
22
  vec3 color = shader_#{@context.name}(frag_coord, resolution, uniforms);
23
- int idx = (y * width + x) * 4;
23
+ size_t idx = ((size_t)y * (size_t)width + (size_t)x) * 4;
24
24
  // Output as BGRA (macOS native format)
25
25
  pixels[idx] = (uint8_t)(clamp_f(color.z, 0.0f, 1.0f) * 255.0f);
26
26
  pixels[idx+1] = (uint8_t)(clamp_f(color.y, 0.0f, 1.0f) * 255.0f);
@@ -29,9 +29,49 @@ module RLSL
29
29
  }
30
30
  }
31
31
 
32
+ typedef struct {
33
+ uint8_t *pixels;
34
+ int width;
35
+ int height;
36
+ vec2 resolution;
37
+ Uniforms uniforms;
38
+ } render_arguments;
39
+
40
+ static void *render_without_gvl(void *opaque) {
41
+ render_arguments *arguments = (render_arguments *)opaque;
42
+
43
+ #ifdef __APPLE__
44
+ dispatch_apply(arguments->height, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t y) {
45
+ render_scanline(arguments->pixels, arguments->width, arguments->height, (int)y,
46
+ arguments->resolution, arguments->uniforms);
47
+ });
48
+ #else
49
+ for (int y = 0; y < arguments->height; y++) {
50
+ render_scanline(arguments->pixels, arguments->width, arguments->height, y,
51
+ arguments->resolution, arguments->uniforms);
52
+ }
53
+ #endif
54
+
55
+ return NULL;
56
+ }
57
+
32
58
  static VALUE shader_#{@context.name}_render(VALUE self, VALUE rb_buffer, VALUE rb_width, VALUE rb_height#{@context.uniform_argument_list}) {
33
- int width = NUM2INT(rb_width);
34
- int height = NUM2INT(rb_height);
59
+ long long requested_width = NUM2LL(rb_width);
60
+ long long requested_height = NUM2LL(rb_height);
61
+ if (requested_width <= 0 || requested_width > INT_MAX ||
62
+ requested_height <= 0 || requested_height > INT_MAX) {
63
+ rb_raise(rb_eArgError, "width and height must be positive integers no greater than INT_MAX");
64
+ }
65
+
66
+ size_t width_size = (size_t)requested_width;
67
+ size_t height_size = (size_t)requested_height;
68
+ if (height_size > SIZE_MAX / width_size || width_size * height_size > SIZE_MAX / 4) {
69
+ rb_raise(rb_eRangeError, "render dimensions overflow the output buffer size");
70
+ }
71
+
72
+ size_t required_bytes = width_size * height_size * 4;
73
+ int width = (int)requested_width;
74
+ int height = (int)requested_height;
35
75
  vec2 resolution = vec2_new((float)width, (float)height);
36
76
 
37
77
  Uniforms uniforms;
@@ -39,17 +79,14 @@ module RLSL
39
79
 
40
80
  Check_Type(rb_buffer, T_STRING);
41
81
  rb_str_modify(rb_buffer);
82
+ if ((size_t)RSTRING_LEN(rb_buffer) < required_bytes) {
83
+ rb_raise(rb_eArgError, "pixel buffer is too small: need %zu bytes, got %ld",
84
+ required_bytes, RSTRING_LEN(rb_buffer));
85
+ }
42
86
  uint8_t *pixels = (uint8_t *)RSTRING_PTR(rb_buffer);
43
87
 
44
- #ifdef __APPLE__
45
- dispatch_apply(height, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t y) {
46
- render_scanline(pixels, width, height, (int)y, resolution, uniforms);
47
- });
48
- #else
49
- for (int y = 0; y < height; y++) {
50
- render_scanline(pixels, width, height, y, resolution, uniforms);
51
- }
52
- #endif
88
+ render_arguments arguments = {pixels, width, height, resolution, uniforms};
89
+ rb_thread_call_without_gvl(render_without_gvl, &arguments, RUBY_UBF_IO, NULL);
53
90
 
54
91
  return Qnil;
55
92
  }
@@ -87,6 +124,9 @@ module RLSL
87
124
 
88
125
  <<~C.strip
89
126
  Check_Type(rb_#{uniform_name}, T_ARRAY);
127
+ if (RARRAY_LEN(rb_#{uniform_name}) != #{vector_size}) {
128
+ rb_raise(rb_eArgError, "uniform #{uniform_name} must contain exactly #{vector_size} components");
129
+ }
90
130
  uniforms.#{uniform_name} = #{constructor}(
91
131
  #{components.join(",\n")}
92
132
  );
@@ -3,10 +3,11 @@
3
3
  module RLSL
4
4
  class CodeGenerator
5
5
  class TemplateContext
6
- attr_reader :name
6
+ attr_reader :name, :extension_name
7
7
 
8
- def initialize(name:, uniforms:, helpers_block:, fragment_block:)
9
- @name = name
8
+ def initialize(name:, uniforms:, helpers_block:, fragment_block:, extension_name: name)
9
+ @name = RLSL.validate_shader_name!(name)
10
+ @extension_name = RLSL.validate_identifier!(extension_name, context: "extension name")
10
11
  @uniforms = uniforms
11
12
  @helpers_block = helpers_block
12
13
  @fragment_block = fragment_block
@@ -23,6 +24,8 @@ module RLSL
23
24
  end
24
25
 
25
26
  def fragment_code
27
+ return "" unless @fragment_block
28
+
26
29
  @fragment_block.call
27
30
  end
28
31