electra 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,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Electra
4
+ # Small typed DSL for vertex and fragment shaders, not a general compiler.
5
+ class Function
6
+ attr_reader :owner, :id, :interfaces, :stage
7
+
8
+ def initialize(owner, stage)
9
+ @owner, @stage = owner, stage
10
+ @interfaces = []
11
+ @bindings = {}
12
+ @finished = false
13
+ @terminated = false
14
+ @id = owner.reserve_id
15
+ void = owner.type(:void)
16
+ signature = owner.type([:function, :void])
17
+ owner.emit(:functions, "OpFunction", void.id, id, :None, signature.id)
18
+ start_block(owner.reserve_id)
19
+ end
20
+
21
+ # @return [Value] Input variable; specify exactly one location or builtin
22
+ def input(type, location: nil, builtin: nil, flat: false)
23
+ define_interface(:Input, type, location:, builtin:, flat:)
24
+ end
25
+
26
+ # @return [Value] writable Output variable
27
+ def output(type, location: nil, builtin: nil)
28
+ define_interface(:Output, type, location:, builtin:)
29
+ end
30
+
31
+ # @return [Value] combined sampled-image descriptor
32
+ def sampled_image_2d(set: 0, binding: 0)
33
+ define_descriptor(owner.type([:sampled_image]), :UniformConstant, set, binding)
34
+ end
35
+
36
+ # Explicit offsets/layout keep GPU ABI decisions visible to the caller.
37
+ def uniform_buffer(*types, set: 0, binding: 0, offsets:)
38
+ define_descriptor(owner.struct(*types, offsets:, block: true), :Uniform, set, binding)
39
+ end
40
+
41
+ # @return [Value] single read-only push-constant block
42
+ def push_constant(*types, offsets:)
43
+ raise Error, "only one push constant block is allowed per shader" if @push_constant
44
+ @push_constant = define_global(owner.struct(*types, offsets:, block: true), :PushConstant)
45
+ @interfaces << @push_constant.id if owner.version >= 0x10400
46
+ @push_constant
47
+ end
48
+
49
+ # @return [Value] struct-member pointer; loads automatically in expressions
50
+ def member(buffer, index)
51
+ raise Error, "expected a struct variable" unless buffer.is_a?(Value) && buffer.storage && buffer.type.kind == :struct
52
+ owner.identifier(buffer)
53
+ raise Error, "buffer belongs to another function" unless buffer.function.equal?(self)
54
+ raise Error, "member out of bounds" unless index.is_a?(Integer) && index.between?(0, buffer.type.members.length - 1)
55
+ type = buffer.type.members[index]
56
+ pointer = owner.type([:pointer, buffer.storage, type])
57
+ access = result("OpAccessChain", pointer, buffer.id, constant(index, :int).id)
58
+ Value.new(owner, self, type, access.id, buffer.storage)
59
+ end
60
+
61
+ # @return [Value] typed constant; infer float/int/bool or float vector if omitted
62
+ def constant(item, type = nil)
63
+ type ||= case item
64
+ when true, false then :bool
65
+ when Integer then :int
66
+ when Numeric then :float
67
+ when Array then "vec#{item.length}".to_sym
68
+ else raise Error, "cannot infer constant type"
69
+ end
70
+ result = owner.constant(type, item)
71
+ Value.new(owner, self, result.type, result.id)
72
+ end
73
+
74
+ def value(item, expected = nil)
75
+ expected = owner.type(expected) if expected
76
+ item = constant(item, expected) unless item.is_a?(Value)
77
+ owner.identifier(item)
78
+ raise Error, "value belongs to another function" if item.function && !item.function.equal?(self)
79
+ raise Error, "type mismatch: expected #{expected.kind}, got #{item.type.kind}" if expected && expected.id != item.type.id
80
+ item.load
81
+ end
82
+
83
+ # @return [Value] composite assembled from typed component values
84
+ def construct(description, *items)
85
+ type = owner.type(description)
86
+ raise Error, "construct supports vectors, matrices, arrays and structs" unless %i[vector matrix array struct].include?(type.kind)
87
+ expected = type.kind == :struct ? type.members.length : type.length
88
+ raise Error, "wrong composite element count" unless items.length == expected
89
+ components = items.each_with_index.map { |item, index| value(item, type.kind == :struct ? type.members[index] : type.element).id }
90
+ result("OpCompositeConstruct", type, *components)
91
+ end
92
+
93
+ # @return [Value] vector containing the scalar repeated count times
94
+ def splat(item, count = 4)
95
+ item = value(item)
96
+ type = owner.type([:vector, item.type, count])
97
+ result("OpCompositeConstruct", type, *Array.new(count, item.id))
98
+ end
99
+
100
+ # @return [Value] vec4 sample; implicit LOD is restricted to fragments
101
+ def sample(texture, uv, lod: nil)
102
+ texture = value(texture)
103
+ raise Error, "expected a sampled 2D image" unless texture.type.kind == :sampled_image
104
+ uv = value(uv, :vec2)
105
+ unless lod.nil?
106
+ result("OpImageSampleExplicitLod", owner.type(:vec4), texture.id, uv.id, :Lod, value(lod, :float).id)
107
+ else
108
+ raise Error, "implicit texture LOD is fragment-only" unless stage == :Fragment
109
+ result("OpImageSampleImplicitLod", owner.type(:vec4), texture.id, uv.id)
110
+ end
111
+ end
112
+
113
+ # @return [Value] component-wise sum of matching numeric operands
114
+ def add(left, right) = arithmetic(:add, left, right)
115
+ # @return [Value] component-wise difference
116
+ def sub(left, right) = arithmetic(:sub, left, right)
117
+ # @return [Value] float, signed or unsigned component-wise quotient
118
+ def div(left, right) = arithmetic(:div, left, right)
119
+ # @return [Value] float/signed modulus or unsigned remainder
120
+ def mod(left, right) = arithmetic(:mod, left, right)
121
+
122
+ # @return [Value] numeric, vector/scalar or dimension-checked matrix product
123
+ def mul(left, right)
124
+ left = value(left)
125
+ if right.is_a?(Numeric) && [:matrix, :vector].include?(left.type.kind) && (left.type.kind == :matrix || left.type.element.kind == :float)
126
+ right = value(right, :float)
127
+ else
128
+ right = value(right, left.type) unless right.is_a?(Value)
129
+ end
130
+ right = value(right)
131
+ kinds = [left.type.kind, right.type.kind]
132
+ if kinds == [:matrix, :vector]
133
+ raise Error, "matrix/vector dimensions differ" unless left.type.length == right.type.length && right.type.element.kind == :float
134
+ result("OpMatrixTimesVector", left.type.element, left.id, right.id)
135
+ elsif kinds == [:vector, :matrix]
136
+ raise Error, "vector/matrix dimensions differ" unless left.type.length == right.type.element.length && left.type.element.kind == :float
137
+ result("OpVectorTimesMatrix", owner.type([:vector, :float, right.type.length]), left.id, right.id)
138
+ elsif kinds == [:matrix, :matrix]
139
+ raise Error, "matrix dimensions differ" unless left.type.length == right.type.element.length
140
+ result("OpMatrixTimesMatrix", owner.type([:matrix, left.type.element, right.type.length]), left.id, right.id)
141
+ elsif [:matrix, :vector].include?(left.type.kind) && right.type.kind == :float && (left.type.kind == :matrix || left.type.element.kind == :float)
142
+ result(left.type.kind == :matrix ? "OpMatrixTimesScalar" : "OpVectorTimesScalar", left.type, left.id, right.id)
143
+ elsif left.type.kind == :float && [:matrix, :vector].include?(right.type.kind)
144
+ mul(right, left)
145
+ else
146
+ arithmetic(:mul, left, right)
147
+ end
148
+ end
149
+
150
+ def negate(item)
151
+ item = value(item)
152
+ raise Error, "negation requires signed numeric type" unless %i[float int].include?(item.type.scalar.kind)
153
+ result(item.type.scalar.kind == :float ? "OpFNegate" : "OpSNegate", item.type, item.id)
154
+ end
155
+
156
+ def dot(left, right)
157
+ left = value(left)
158
+ right = value(right, left.type)
159
+ raise Error, "dot requires float vectors" unless left.type.kind == :vector && left.type.element.kind == :float
160
+ result("OpDot", owner.type(:float), left.id, right.id)
161
+ end
162
+
163
+ def transpose(item)
164
+ item = value(item)
165
+ raise Error, "transpose requires a matrix" unless item.type.kind == :matrix
166
+ type = owner.type([:matrix, [:vector, :float, item.type.length], item.type.element.length])
167
+ result("OpTranspose", type, item.id)
168
+ end
169
+
170
+ def convert(item, description)
171
+ item = value(item)
172
+ type = owner.type(description)
173
+ from, to = item.type.scalar.kind, type.scalar.kind
174
+ raise Error, "conversion dimensions differ" unless item.type.length == type.length
175
+ return item if item.type.id == type.id
176
+ opcode = {[:float, :int] => "OpConvertFToS", [:float, :uint] => "OpConvertFToU", [:int, :float] => "OpConvertSToF", [:uint, :float] => "OpConvertUToF", [:int, :uint] => "OpBitcast", [:uint, :int] => "OpBitcast"}[[from, to]]
177
+ raise Error, "unsupported conversion" unless opcode
178
+ result(opcode, type, item.id)
179
+ end
180
+
181
+ # @return [Value] scalar/vector equality (ordered for floating-point inputs)
182
+ def equal(left, right) = compare(:equal, left, right)
183
+ # @return [Value] scalar/vector ordered inequality
184
+ def not_equal(left, right) = compare(:not_equal, left, right)
185
+ # @return [Value] component-wise less-than comparison
186
+ def less_than(left, right) = compare(:less_than, left, right)
187
+ # @return [Value] component-wise less-than-or-equal comparison
188
+ def less_equal(left, right) = compare(:less_equal, left, right)
189
+ # @return [Value] component-wise greater-than comparison
190
+ def greater_than(left, right) = compare(:greater_than, left, right)
191
+ # @return [Value] component-wise greater-than-or-equal comparison
192
+ def greater_equal(left, right) = compare(:greater_equal, left, right)
193
+
194
+ # @return [Value] component-wise boolean conjunction
195
+ def logical_and(left, right) = logical("OpLogicalAnd", left, right)
196
+ # @return [Value] component-wise boolean disjunction
197
+ def logical_or(left, right) = logical("OpLogicalOr", left, right)
198
+ def logical_not(item)
199
+ item = value(item)
200
+ raise Error, "logical operation requires bool" unless item.type.scalar.kind == :bool
201
+ result("OpLogicalNot", item.type, item.id)
202
+ end
203
+
204
+ def select(condition, yes, no)
205
+ condition = value(condition)
206
+ yes = value(yes)
207
+ no = value(no, yes.type)
208
+ expected = yes.type.kind == :vector ? owner.type([:vector, :bool, yes.type.length]) : owner.type(:bool)
209
+ condition = splat(condition, yes.type.length) if condition.type.kind == :bool && yes.type.kind == :vector
210
+ raise Error, "select condition has incompatible type" unless condition.type.id == expected.id
211
+ result("OpSelect", yes.type, condition.id, yes.id, no.id)
212
+ end
213
+
214
+ # Branch callbacks may return a value; matching results become an OpPhi.
215
+ # With no returned value this is ordinary structured conditional control.
216
+ def if_else(condition, yes, no = nil)
217
+ condition = value(condition, :bool)
218
+ then_id, else_id, merge_id = 3.times.map { owner.reserve_id }
219
+ emit("OpSelectionMerge", merge_id, :None)
220
+ emit("OpBranchConditional", condition.id, then_id, else_id)
221
+ then_value, then_label, then_reachable = build_branch(then_id, merge_id) { yes.call(self) }
222
+ else_value, else_label, else_reachable = build_branch(else_id, merge_id) { no&.call(self) }
223
+ start_block(merge_id)
224
+ if then_reachable && else_reachable && then_value.is_a?(Value) && else_value.is_a?(Value)
225
+ raise Error, "branch result types differ" unless then_value.type.id == else_value.type.id
226
+ result("OpPhi", then_value.type, then_value.id, then_label, else_value.id, else_label)
227
+ elsif !then_reachable && !else_reachable
228
+ emit("OpUnreachable")
229
+ nil
230
+ end
231
+ end
232
+
233
+ def discard
234
+ raise Error, "discard is fragment-only" unless stage == :Fragment
235
+ emit("OpKill")
236
+ nil
237
+ end
238
+
239
+ # Discard the fragment when the scalar boolean condition is true.
240
+ # @return [nil]
241
+ def discard_if(condition)
242
+ if_else(condition, ->(function) { function.discard })
243
+ nil
244
+ end
245
+
246
+ # Terminate the current block with a void return.
247
+ # @return [nil]
248
+ def return_void
249
+ emit("OpReturn")
250
+ nil
251
+ end
252
+
253
+ # GLSL.std.450 operations needed for UI gradients, clipping and transforms.
254
+ def ext(operation, *items)
255
+ names = {round: "Round", abs: "FAbs", floor: "Floor", ceil: "Ceil", fract: "Fract", sin: "Sin", cos: "Cos", pow: "Pow", exp: "Exp", log: "Log", sqrt: "Sqrt", inverse_sqrt: "InverseSqrt", min: "FMin", max: "FMax", clamp: "FClamp", mix: "FMix", step: "Step", smoothstep: "SmoothStep", length: "Length", normalize: "Normalize"}
256
+ arities = {pow: 2, min: 2, max: 2, clamp: 3, mix: 3, step: 2, smoothstep: 3}
257
+ name = names[operation] or raise Error, "unknown GLSL operation #{operation}"
258
+ opcode = GLSL_INSTRUCTIONS.fetch(name)
259
+ raise Error, "wrong GLSL operand count" unless items.length == arities.fetch(operation, 1)
260
+ first = value(items.first)
261
+ raise Error, "GLSL operation requires float scalar/vector" unless first.type.scalar.kind == :float
262
+ operands = [first, *items.drop(1).map { |item| value(item, first.type) }]
263
+ type = operation == :length ? owner.type(:float) : first.type
264
+ result("OpExtInst", type, owner.import("GLSL.std.450"), opcode, *operands.map(&:id))
265
+ end
266
+
267
+ def emit(opcode, *operands)
268
+ raise Error, "function already finished" if @finished
269
+ raise Error, "cannot emit after block terminator" if @terminated
270
+ owner.emit(:functions, opcode, *operands)
271
+ @terminated = true if %w[OpBranch OpBranchConditional OpSwitch OpKill OpReturn OpReturnValue OpUnreachable].include?(opcode)
272
+ self
273
+ end
274
+
275
+ # Low-level typed instruction with an automatically allocated result ID.
276
+ # @return [Value]
277
+ def result(opcode, type, *operands)
278
+ type = owner.type(type)
279
+ id = owner.reserve_id
280
+ emit(opcode, type.id, id, *operands)
281
+ Value.new(owner, self, type, id)
282
+ end
283
+
284
+ # Close the function, inserting a return for an unterminated block.
285
+ # @return [self]
286
+ def finish
287
+ return self if @finished
288
+ emit("OpReturn") unless @terminated
289
+ owner.emit(:functions, "OpFunctionEnd")
290
+ @finished = true
291
+ self
292
+ end
293
+
294
+ def finished? = @finished
295
+
296
+ private
297
+
298
+ def build_branch(label_id, merge_id)
299
+ start_block(label_id)
300
+ branch_value = yield
301
+ branch_label = @current_label
302
+ reachable = !@terminated
303
+ branch_value = value(branch_value) if reachable && branch_value.is_a?(Value)
304
+ emit("OpBranch", merge_id) if reachable
305
+ [branch_value, branch_label, reachable]
306
+ end
307
+
308
+ def start_block(id)
309
+ owner.emit(:functions, "OpLabel", id)
310
+ @current_label = id
311
+ @terminated = false
312
+ end
313
+
314
+ def define_interface(storage, description, location: nil, builtin: nil, flat: false)
315
+ raise Error, "provide exactly one of location or builtin" unless location.nil? != builtin.nil?
316
+ type = owner.type(description)
317
+ key = [storage, location, builtin]
318
+ raise Error, "duplicate shader interface" if @bindings[key]
319
+ @bindings[key] = true
320
+ variable = define_global(type, storage)
321
+ owner.decorate(variable, :Location, location) unless location.nil?
322
+ owner.decorate(variable, :BuiltIn, builtin) unless builtin.nil?
323
+ owner.decorate(variable, :Flat) if flat
324
+ @interfaces << variable.id
325
+ variable
326
+ end
327
+
328
+ def define_descriptor(type, storage, set, binding)
329
+ key = [set, binding]
330
+ raise Error, "duplicate descriptor binding" if @bindings[key]
331
+ @bindings[key] = true
332
+ variable = define_global(type, storage)
333
+ owner.decorate(variable, :DescriptorSet, set)
334
+ owner.decorate(variable, :Binding, binding)
335
+ @interfaces << variable.id if owner.version >= 0x10400
336
+ variable
337
+ end
338
+
339
+ def define_global(type, storage)
340
+ pointer = owner.type([:pointer, storage, type])
341
+ id = owner.reserve_id
342
+ owner.emit(:declarations, "OpVariable", pointer.id, id, storage)
343
+ Value.new(owner, self, type, id, storage)
344
+ end
345
+
346
+ def arithmetic(operation, left, right)
347
+ left = value(left)
348
+ right = value(right, left.type)
349
+ kind = left.type.scalar.kind
350
+ raise Error, "arithmetic requires numeric scalars or vectors" unless %i[float int uint].include?(kind)
351
+ codes = {add: %w[OpFAdd OpIAdd OpIAdd], sub: %w[OpFSub OpISub OpISub], mul: %w[OpFMul OpIMul OpIMul], div: %w[OpFDiv OpSDiv OpUDiv], mod: %w[OpFMod OpSMod OpUMod]}
352
+ result(codes.fetch(operation)[%i[float int uint].index(kind)], left.type, left.id, right.id)
353
+ end
354
+
355
+ def compare(operation, left, right)
356
+ left = value(left)
357
+ right = value(right, left.type)
358
+ kind = left.type.scalar.kind
359
+ endings = {equal: "Equal", not_equal: "NotEqual", less_than: "LessThan", less_equal: "LessThanEqual", greater_than: "GreaterThan", greater_equal: "GreaterThanEqual"}
360
+ prefix = case kind
361
+ when :float then "FOrd"
362
+ when :int then %i[equal not_equal].include?(operation) ? "I" : "S"
363
+ when :uint then %i[equal not_equal].include?(operation) ? "I" : "U"
364
+ when :bool
365
+ raise Error, "booleans are not ordered" unless %i[equal not_equal].include?(operation)
366
+ "Logical"
367
+ else raise Error, "comparison requires scalars or vectors"
368
+ end
369
+ type = left.type.kind == :vector ? owner.type([:vector, :bool, left.type.length]) : owner.type(:bool)
370
+ result("Op#{prefix}#{endings.fetch(operation)}", type, left.id, right.id)
371
+ end
372
+
373
+ def logical(opcode, left, right)
374
+ left = value(left)
375
+ right = value(right, left.type)
376
+ raise Error, "logical operation requires bool" unless left.type.scalar.kind == :bool
377
+ result(opcode, left.type, left.id, right.id)
378
+ end
379
+ end
380
+ end