rlsl 0.1.0

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.
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ module Prism
5
+ class TypeInference
6
+ attr_reader :symbol_table
7
+
8
+ def initialize(uniforms = {}, custom_functions = {})
9
+ @symbol_table = {}
10
+ @uniforms = uniforms
11
+ @custom_functions = custom_functions
12
+
13
+ uniforms.each do |name, type|
14
+ @symbol_table[name.to_sym] = type
15
+ end
16
+ end
17
+
18
+ def register(name, type)
19
+ @symbol_table[name.to_sym] = type
20
+ end
21
+
22
+ def register_function(name, returns:)
23
+ @custom_functions[name.to_sym] = { returns: returns }
24
+ end
25
+
26
+ def lookup(name)
27
+ @symbol_table[name.to_sym]
28
+ end
29
+
30
+ def infer(node)
31
+ case node
32
+ when IR::Block
33
+ infer_block(node)
34
+ when IR::VarDecl
35
+ infer_var_decl(node)
36
+ when IR::VarRef
37
+ infer_var_ref(node)
38
+ when IR::Literal
39
+ infer_literal(node)
40
+ when IR::BoolLiteral
41
+ node.type = :bool
42
+ node
43
+ when IR::BinaryOp
44
+ infer_binary_op(node)
45
+ when IR::UnaryOp
46
+ infer_unary_op(node)
47
+ when IR::FuncCall
48
+ infer_func_call(node)
49
+ when IR::FieldAccess
50
+ infer_field_access(node)
51
+ when IR::Swizzle
52
+ infer_swizzle(node)
53
+ when IR::IfStatement
54
+ infer_if_statement(node)
55
+ when IR::Return
56
+ infer_return(node)
57
+ when IR::Assignment
58
+ infer_assignment(node)
59
+ when IR::ForLoop
60
+ infer_for_loop(node)
61
+ when IR::WhileLoop
62
+ infer_while_loop(node)
63
+ when IR::Parenthesized
64
+ infer_parenthesized(node)
65
+ when IR::FunctionDefinition
66
+ infer_function_definition(node)
67
+ when IR::ArrayLiteral
68
+ infer_array_literal(node)
69
+ when IR::ArrayIndex
70
+ infer_array_index(node)
71
+ when IR::GlobalDecl
72
+ infer_global_decl(node)
73
+ when IR::MultipleAssignment
74
+ infer_multiple_assignment(node)
75
+ else
76
+ node
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def infer_block(node)
83
+ node.statements.each { |stmt| infer(stmt) }
84
+ node.type = node.statements.last&.type
85
+ node
86
+ end
87
+
88
+ def infer_var_decl(node)
89
+ infer(node.initializer) if node.initializer
90
+ node.type ||= node.initializer&.type
91
+ register(node.name, node.type) if node.type
92
+ node
93
+ end
94
+
95
+ def infer_var_ref(node)
96
+ node.type ||= lookup(node.name)
97
+ node
98
+ end
99
+
100
+ def infer_literal(node)
101
+ node
102
+ end
103
+
104
+ def infer_binary_op(node)
105
+ infer(node.left)
106
+ infer(node.right)
107
+
108
+ node.type = Builtins.binary_op_result_type(
109
+ node.operator,
110
+ node.left.type,
111
+ node.right.type
112
+ )
113
+ node
114
+ end
115
+
116
+ def infer_unary_op(node)
117
+ infer(node.operand)
118
+
119
+ case node.operator.to_s
120
+ when "-"
121
+ node.type = node.operand.type
122
+ when "!"
123
+ node.type = :bool
124
+ end
125
+ node
126
+ end
127
+
128
+ def infer_func_call(node)
129
+ node.args.each { |arg| infer(arg) }
130
+ infer(node.receiver) if node.receiver
131
+
132
+ sig = Builtins.function_signature(node.name)
133
+ if sig
134
+ arg_types = node.args.map(&:type)
135
+ node.type = Builtins.resolve_return_type(sig[:returns], arg_types)
136
+ elsif @custom_functions.key?(node.name.to_sym)
137
+ node.type = @custom_functions[node.name.to_sym][:returns]
138
+ else
139
+ node.type = node.receiver&.type
140
+ end
141
+ node
142
+ end
143
+
144
+ def infer_field_access(node)
145
+ infer(node.receiver)
146
+
147
+ if Builtins.single_component_field?(node.field)
148
+ node.type = :float
149
+ else
150
+ node.type = @uniforms[node.field.to_sym] || :float
151
+ end
152
+ node
153
+ end
154
+
155
+ def infer_swizzle(node)
156
+ infer(node.receiver)
157
+ node.type = Builtins.swizzle_type(node.components)
158
+ node
159
+ end
160
+
161
+ def infer_if_statement(node)
162
+ infer(node.condition)
163
+ infer(node.then_branch)
164
+ infer(node.else_branch) if node.else_branch
165
+
166
+ node.type = node.then_branch.type
167
+ node
168
+ end
169
+
170
+ def infer_return(node)
171
+ infer(node.expression) if node.expression
172
+ node.type = node.expression&.type
173
+ node
174
+ end
175
+
176
+ def infer_assignment(node)
177
+ infer(node.target)
178
+ infer(node.value)
179
+ node.type = node.value.type
180
+ node
181
+ end
182
+
183
+ def infer_for_loop(node)
184
+ register(node.variable, :int)
185
+ infer(node.range_start)
186
+ infer(node.range_end)
187
+ infer(node.body)
188
+ node.type = nil
189
+ node
190
+ end
191
+
192
+ def infer_while_loop(node)
193
+ infer(node.condition)
194
+ infer(node.body)
195
+ node.type = nil
196
+ node
197
+ end
198
+
199
+ def infer_function_definition(node)
200
+ node.param_types.each do |param_name, param_type|
201
+ register(param_name, param_type)
202
+ end
203
+
204
+ infer(node.body)
205
+
206
+ node.return_type ||= node.body&.type
207
+ node.type = node.return_type
208
+ node
209
+ end
210
+
211
+ def infer_parenthesized(node)
212
+ infer(node.expression)
213
+ node.type = node.expression.type
214
+ node
215
+ end
216
+
217
+ def infer_array_literal(node)
218
+ node.elements.each { |elem| infer(elem) }
219
+
220
+ element_type = node.elements.first&.type || :float
221
+ node.type = :"array_#{element_type}"
222
+ node
223
+ end
224
+
225
+ def infer_array_index(node)
226
+ infer(node.array)
227
+ infer(node.index)
228
+
229
+ array_type = node.array.type
230
+ if array_type.to_s.start_with?("array_")
231
+ node.type = array_type.to_s.sub("array_", "").to_sym
232
+ else
233
+ node.type = @symbol_table["#{node.array.name}_element_type".to_sym] || :float
234
+ end
235
+ node
236
+ end
237
+
238
+ def infer_global_decl(node)
239
+ infer(node.initializer) if node.initializer
240
+
241
+ if node.initializer.is_a?(IR::ArrayLiteral)
242
+ node.array_size ||= node.initializer.elements.length
243
+ first_elem = node.initializer.elements.first
244
+ node.element_type ||= first_elem&.type || :float
245
+ node.type = :"array_#{node.element_type}"
246
+ else
247
+ node.type ||= node.initializer&.type
248
+ end
249
+
250
+ register(node.name, node.type) if node.type
251
+
252
+ if node.element_type
253
+ register("#{node.name}_element_type".to_sym, node.element_type)
254
+ end
255
+
256
+ node
257
+ end
258
+
259
+ def infer_multiple_assignment(node)
260
+ infer(node.value)
261
+
262
+ value_type = node.value.type
263
+ if value_type.is_a?(IR::TupleType)
264
+ node.targets.each_with_index do |target, i|
265
+ target.type = value_type.types[i]
266
+ register(target.name, target.type)
267
+ end
268
+ elsif value_type.to_s.start_with?("array_")
269
+ elem_type = value_type.to_s.sub("array_", "").to_sym
270
+ node.targets.each do |target|
271
+ target.type = elem_type
272
+ register(target.name, target.type)
273
+ end
274
+ elsif @custom_functions.key?(node.value.name) && node.value.is_a?(IR::FuncCall)
275
+ func_info = @custom_functions[node.value.name]
276
+ if func_info[:returns].is_a?(Array)
277
+ node.targets.each_with_index do |target, i|
278
+ target.type = func_info[:returns][i]
279
+ register(target.name, target.type)
280
+ end
281
+ end
282
+ end
283
+
284
+ node.type = nil
285
+ node
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+ require "rbconfig"
6
+
7
+ module RLSL
8
+ class ShaderBuilder
9
+ attr_reader :name
10
+
11
+ def initialize(name)
12
+ @name = name.to_s
13
+ @uniforms = {}
14
+ @fragment_mode = :c
15
+ @helpers_mode = :c
16
+ @custom_functions = {}
17
+ end
18
+
19
+ def uniforms(&block)
20
+ if block_given?
21
+ ctx = UniformContext.new
22
+ ctx.instance_eval(&block)
23
+ @uniforms = ctx.uniforms
24
+ else
25
+ @uniforms
26
+ end
27
+ end
28
+
29
+ def helpers(mode = :ruby, &block)
30
+ @helpers_block = block
31
+ @helpers_mode = mode
32
+ end
33
+
34
+ def functions(&block)
35
+ ctx = FunctionContext.new
36
+ ctx.instance_eval(&block)
37
+ @custom_functions = ctx.functions
38
+ end
39
+
40
+ def fragment(&block)
41
+ @fragment_block = block
42
+ @fragment_mode = block.arity > 0 ? :ruby : :c
43
+ end
44
+
45
+ def ruby_mode?
46
+ @fragment_mode == :ruby
47
+ end
48
+
49
+ def compile_and_load
50
+ c_code = generate_c_code
51
+ code_hash = Digest::MD5.hexdigest(c_code)[0..7]
52
+ ext_name = "#{@name}_#{code_hash}"
53
+ ext_dir = File.join(RLSL.cache_dir, ext_name)
54
+ ext_file = File.join(ext_dir, "#{@name}.#{RbConfig::CONFIG['DLEXT']}")
55
+
56
+ unless File.exist?(ext_file)
57
+ compile_extension(@name, ext_dir, c_code)
58
+ end
59
+
60
+ require ext_file
61
+ CompiledShader.new(@name, ext_name, @uniforms.keys)
62
+ end
63
+
64
+ def build_metal_shader
65
+ if ruby_mode?
66
+ fragment_code = transpile_fragment(:msl)
67
+ helpers_code = @helpers_block ? @helpers_block.call : ""
68
+ else
69
+ helpers_code = @helpers_block ? @helpers_block.call : ""
70
+ fragment_code = @fragment_block ? @fragment_block.call : ""
71
+ end
72
+
73
+ translator = MSL::Translator.new(@uniforms, helpers_code, fragment_code)
74
+ msl_source = translator.translate
75
+
76
+ MSL::Shader.new(@name, @uniforms, msl_source)
77
+ end
78
+
79
+ def build_wgsl_shader
80
+ if ruby_mode?
81
+ fragment_code = transpile_fragment(:wgsl)
82
+ helpers_code = @helpers_block ? @helpers_block.call : ""
83
+ else
84
+ helpers_code = @helpers_block ? @helpers_block.call : ""
85
+ fragment_code = @fragment_block ? @fragment_block.call : ""
86
+ end
87
+
88
+ translator = WGSL::Translator.new(@uniforms, helpers_code, fragment_code)
89
+ translator.translate
90
+ end
91
+
92
+ def build_glsl_shader(version: "450")
93
+ if ruby_mode?
94
+ fragment_code = transpile_fragment(:glsl)
95
+ helpers_code = @helpers_block ? @helpers_block.call : ""
96
+ else
97
+ helpers_code = @helpers_block ? @helpers_block.call : ""
98
+ fragment_code = @fragment_block ? @fragment_block.call : ""
99
+ end
100
+
101
+ translator = GLSL::Translator.new(@uniforms, helpers_code, fragment_code, version: version)
102
+ translator.translate
103
+ end
104
+
105
+ def transpile_fragment(target)
106
+ return "" unless @fragment_block
107
+
108
+ transpiler = Prism::Transpiler.new(@uniforms, @custom_functions)
109
+ transpiler.transpile(@fragment_block, target)
110
+ end
111
+
112
+ def transpile_helpers(target)
113
+ return "" unless @helpers_block
114
+
115
+ transpiler = Prism::Transpiler.new(@uniforms, @custom_functions)
116
+ transpiler.transpile_helpers(@helpers_block, target, @custom_functions)
117
+ end
118
+
119
+ def helpers_ruby_mode?
120
+ @helpers_mode == :ruby
121
+ end
122
+
123
+ private
124
+
125
+ def generate_c_code
126
+ if helpers_ruby_mode?
127
+ helpers_code = transpile_helpers(:c)
128
+ helpers_block = -> { helpers_code }
129
+ else
130
+ helpers_block = @helpers_block
131
+ end
132
+
133
+ if ruby_mode?
134
+ fragment_code = transpile_fragment(:c)
135
+ fragment_block = -> { fragment_code }
136
+ else
137
+ fragment_block = @fragment_block
138
+ end
139
+
140
+ codegen = CodeGenerator.new(@name, @uniforms, helpers_block, fragment_block)
141
+ codegen.generate
142
+ end
143
+
144
+ def compile_extension(ext_name, ext_dir, c_code)
145
+ FileUtils.mkdir_p(ext_dir)
146
+
147
+ File.write(File.join(ext_dir, "#{ext_name}.c"), c_code)
148
+
149
+ extconf = <<~RUBY
150
+ require "mkmf"
151
+ $CFLAGS << " -O3 -ffast-math"
152
+ if RUBY_PLATFORM =~ /darwin/
153
+ $CFLAGS << " -fblocks"
154
+ end
155
+ create_makefile("#{ext_name}")
156
+ RUBY
157
+ File.write(File.join(ext_dir, "extconf.rb"), extconf)
158
+
159
+ Dir.chdir(ext_dir) do
160
+ system("#{RbConfig.ruby} extconf.rb > /dev/null 2>&1") or raise "extconf failed for #{ext_name}"
161
+ system("/usr/bin/make > /dev/null 2>&1") or raise "make failed for #{ext_name}"
162
+ end
163
+ end
164
+ end
165
+ end
data/lib/rlsl/types.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ # C type definitions for the shader system
5
+ C_TYPES = <<~C
6
+ typedef struct { float x, y; } vec2;
7
+ typedef struct { float x, y, z; } vec3;
8
+ typedef struct { float x, y, z, w; } vec4;
9
+ typedef struct { float m[4]; } mat2;
10
+ typedef struct { float m[9]; } mat3;
11
+ typedef struct { float m[16]; } mat4;
12
+ typedef struct { void* data; int width; int height; } sampler2D;
13
+
14
+ #define PI 3.14159265f
15
+ #define TAU 6.28318530f
16
+ C
17
+
18
+ # Uniform type symbols
19
+ UNIFORM_TYPES = %i[float vec2 vec3 vec4 int bool mat2 mat3 mat4 sampler2D].freeze
20
+
21
+ # Type mapping helper
22
+ module TypeMapping
23
+ C_UNIFORM_TYPES = {
24
+ float: "float",
25
+ int: "int",
26
+ bool: "int",
27
+ vec2: "vec2",
28
+ vec3: "vec3",
29
+ vec4: "vec4",
30
+ mat2: "mat2",
31
+ mat3: "mat3",
32
+ mat4: "mat4",
33
+ sampler2D: "sampler2D"
34
+ }.freeze
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ # DSL context for defining uniform variables
5
+ class UniformContext
6
+ attr_reader :uniforms
7
+
8
+ def initialize
9
+ @uniforms = {}
10
+ end
11
+
12
+ def float(name)
13
+ @uniforms[name] = :float
14
+ end
15
+
16
+ def vec2(name)
17
+ @uniforms[name] = :vec2
18
+ end
19
+
20
+ def vec3(name)
21
+ @uniforms[name] = :vec3
22
+ end
23
+
24
+ def vec4(name)
25
+ @uniforms[name] = :vec4
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ module WGSL
5
+ class Translator < BaseTranslator
6
+ TYPE_MAP = {
7
+ "vec2" => "vec2<f32>",
8
+ "vec3" => "vec3<f32>",
9
+ "vec4" => "vec4<f32>"
10
+ }.freeze
11
+
12
+ FUNC_REPLACEMENTS = BaseTranslator.common_func_replacements(
13
+ target_vec2: "vec2<f32>",
14
+ target_vec3: "vec3<f32>",
15
+ target_vec4: "vec4<f32>"
16
+ ).freeze
17
+
18
+ protected
19
+
20
+ def translate_code(c_code)
21
+ result = super(c_code)
22
+ return result if result.empty?
23
+
24
+ result.gsub!(/\bstatic\s+/, "")
25
+ result.gsub!(/\binline\s+/, "")
26
+ result.gsub!(/\bfloat\b/, "f32")
27
+ result
28
+ end
29
+
30
+ def generate_shader(helpers, fragment)
31
+ <<~WGSL
32
+ // WGSL Shader generated by RLSL
33
+
34
+ struct Uniforms {
35
+ #{generate_uniform_struct}
36
+ };
37
+
38
+ @group(0) @binding(0) var<uniform> u: Uniforms;
39
+ @group(0) @binding(1) var output_texture: texture_storage_2d<rgba8unorm, write>;
40
+
41
+ #{helpers}
42
+
43
+ fn shader_fragment(frag_coord: vec2<f32>, resolution: vec2<f32>) -> vec3<f32> {
44
+ let uv = frag_coord / resolution.y;
45
+ #{fragment}
46
+ }
47
+
48
+ @compute @workgroup_size(8, 8)
49
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
50
+ let resolution = vec2<f32>(f32(textureDimensions(output_texture).x),
51
+ f32(textureDimensions(output_texture).y));
52
+ if (gid.x >= u32(resolution.x) || gid.y >= u32(resolution.y)) {
53
+ return;
54
+ }
55
+
56
+ let frag_coord = vec2<f32>(f32(gid.x), resolution.y - 1.0 - f32(gid.y));
57
+ let color = shader_fragment(frag_coord, resolution);
58
+
59
+ textureStore(output_texture, vec2<i32>(i32(gid.x), i32(gid.y)),
60
+ vec4<f32>(clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0));
61
+ }
62
+ WGSL
63
+ end
64
+
65
+ private
66
+
67
+ def generate_uniform_struct
68
+ fields = ["resolution: vec2<f32>,"]
69
+ @uniforms.each do |name, type|
70
+ wgsl_type = uniform_type_to_target(type)
71
+ fields << "#{name}: #{wgsl_type},"
72
+ end
73
+ fields.join("\n ")
74
+ end
75
+
76
+ def target_float_type
77
+ "f32"
78
+ end
79
+
80
+ def target_vec2_type
81
+ "vec2<f32>"
82
+ end
83
+
84
+ def target_vec3_type
85
+ "vec3<f32>"
86
+ end
87
+
88
+ def target_vec4_type
89
+ "vec4<f32>"
90
+ end
91
+ end
92
+ end
93
+ end
data/lib/rlsl.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ require_relative "rlsl/version"
6
+ require_relative "rlsl/types"
7
+ require_relative "rlsl/uniform_context"
8
+ require_relative "rlsl/function_context"
9
+ require_relative "rlsl/code_generator"
10
+ require_relative "rlsl/compiled_shader"
11
+ require_relative "rlsl/base_translator"
12
+ require_relative "rlsl/msl/translator"
13
+ require_relative "rlsl/msl/shader"
14
+ require_relative "rlsl/wgsl/translator"
15
+ require_relative "rlsl/glsl/translator"
16
+ require_relative "rlsl/prism/transpiler"
17
+ require_relative "rlsl/shader_builder"
18
+
19
+ module RLSL
20
+ CACHE_DIR = File.expand_path("~/.cache/rlsl/compiled")
21
+
22
+ class << self
23
+ def define(name, &block)
24
+ builder = ShaderBuilder.new(name)
25
+ builder.instance_eval(&block)
26
+ builder.compile_and_load
27
+ end
28
+
29
+ def define_metal(name, &block)
30
+ builder = ShaderBuilder.new(name)
31
+ builder.instance_eval(&block)
32
+ builder.build_metal_shader
33
+ end
34
+
35
+ def to_wgsl(name, &block)
36
+ builder = ShaderBuilder.new(name)
37
+ builder.instance_eval(&block)
38
+ builder.build_wgsl_shader
39
+ end
40
+
41
+ def to_glsl(name, version: "450", &block)
42
+ builder = ShaderBuilder.new(name)
43
+ builder.instance_eval(&block)
44
+ builder.build_glsl_shader(version: version)
45
+ end
46
+
47
+ def cache_dir
48
+ @cache_dir ||= begin
49
+ FileUtils.mkdir_p(CACHE_DIR)
50
+ CACHE_DIR
51
+ end
52
+ end
53
+ end
54
+
55
+ module CompiledShaders
56
+ end
57
+ end