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,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Electra
4
+ # Ordered SPIR-V module builder. IDs belong to a module, never a global pool.
5
+ class Module
6
+ # SPIR-V logical module layout order.
7
+ SECTIONS = %i[capabilities extensions imports memory_model entry_points execution_modes debug annotations declarations functions].freeze
8
+ attr_reader :version
9
+
10
+ # @param version [String] SPIR-V version, 1.0 through 1.6
11
+ # @param generator [Integer] 32-bit generator word; zero means unregistered
12
+ def initialize(version: "1.0", generator: 0)
13
+ major, minor = version.to_s.split(".").map(&:to_i)
14
+ raise Error, "supported versions are 1.0 through 1.6" unless major == 1 && minor && minor.between?(0, 6) && version.to_s == "#{major}.#{minor}"
15
+ @version = (major << 16) | (minor << 8)
16
+ @generator = Binary.word(generator)
17
+ @sections = SECTIONS.to_h { |section| [section, []] }
18
+ @next_id = 1
19
+ @types = {}
20
+ @constants = {}
21
+ @names = {}
22
+ @definitions = {}
23
+ @capabilities = {}
24
+ @extensions = {}
25
+ @imports = {}
26
+ @decorations = {}
27
+ @entries = {}
28
+ @functions = []
29
+ capability(:Shader)
30
+ memory_model(:Logical, :GLSL450)
31
+ end
32
+
33
+ # Allocate a module-local forward ID. A named reservation is idempotent.
34
+ # @param name [Object, nil] optional lookup key
35
+ # @return [Integer] SSA identifier, which must later be defined exactly once
36
+ def reserve_id(name = nil)
37
+ return @names[name] if name && @names.key?(name)
38
+ raise Error, "SPIR-V id limit exceeded" if @next_id >= Binary::MAX_BOUND
39
+ id = @next_id
40
+ @next_id += 1
41
+ @names[name] = id if name
42
+ id
43
+ end
44
+ alias ref reserve_id
45
+
46
+ # Low-level escape hatch. Operands follow the Khronos grammar order,
47
+ # including result type/result IDs. Reserve forward IDs with #ref first.
48
+ def emit(section, opcode, *operands)
49
+ buffer = @sections[section] or raise Error, "unknown section #{section}"
50
+ words = Binary.encode(opcode.to_s, operands)
51
+ schema = INSTRUCTIONS.fetch(opcode.to_s)[1]
52
+ index = schema.index { |kind, _| kind == "IdResult" }
53
+ if index
54
+ id = operands[index].to_i
55
+ raise Error, "id #{id} was not reserved in this module" unless id.between?(1, @next_id - 1)
56
+ raise Error, "id #{id} already defined" if @definitions[id]
57
+ @definitions[id] = true
58
+ end
59
+ buffer.concat(words)
60
+ self
61
+ end
62
+
63
+ # Add a capability once, using its Khronos spelling.
64
+ # @return [self]
65
+ def capability(name)
66
+ key = Binary.enum("Capability", name)[0]
67
+ emit(:capabilities, "OpCapability", name) unless @capabilities[key]
68
+ @capabilities[key] = true
69
+ self
70
+ end
71
+
72
+ # Add an OpExtension declaration once.
73
+ # @return [self]
74
+ def extension(name)
75
+ emit(:extensions, "OpExtension", name) unless @extensions[name]
76
+ @extensions[name] = true
77
+ self
78
+ end
79
+
80
+ # @return [Integer] deduplicated extended-instruction set ID
81
+ def import(name)
82
+ @imports[name] ||= reserve_id.tap { |id| emit(:imports, "OpExtInstImport", id, name) }
83
+ end
84
+
85
+ # Replace the module's single addressing/memory-model declaration.
86
+ # @return [self]
87
+ def memory_model(addressing = :Logical, model = :GLSL450)
88
+ @sections[:memory_model] = Binary.encode("OpMemoryModel", [addressing, model])
89
+ self
90
+ end
91
+
92
+ # Attach a debug name; does not change numeric ID allocation.
93
+ # @return [self]
94
+ def name(target, text)
95
+ emit(:debug, "OpName", identifier(target), text)
96
+ end
97
+
98
+ # Add a decoration. Conflicting values for the same decoration are errors.
99
+ # @return [self]
100
+ def decorate(target, decoration, *values)
101
+ apply_decoration("OpDecorate", identifier(target), decoration, values)
102
+ end
103
+
104
+ # Decorate a struct member, for example Offset or MatrixStride.
105
+ # @return [self]
106
+ def member_decorate(target, member, decoration, *values)
107
+ apply_decoration("OpMemberDecorate", identifier(target), decoration, values, member)
108
+ end
109
+
110
+ # Resolve and structurally intern a type description.
111
+ # @param description [Symbol, String, Array, Type] scalar/composite type
112
+ # @return [Type] immutable type record owned by this module
113
+ def type(description)
114
+ if description.is_a?(Type)
115
+ raise Error, "type belongs to another module" unless description.owner.equal?(self)
116
+ return description
117
+ end
118
+ raise Error, "type must be a Symbol, String, Type or Array" unless description.is_a?(Array) || description.is_a?(Symbol) || description.is_a?(String)
119
+ key = description.is_a?(Array) ? description.dup.freeze : description.to_sym
120
+ return @types[key] if @types.key?(key)
121
+ if (alias_description = expand_type_alias(key))
122
+ return @types[key] = type(alias_description)
123
+ end
124
+ define_type(key, description)
125
+ end
126
+
127
+ # @return [Type] fixed-size array with optional explicit ArrayStride
128
+ def array(element, length, stride: nil)
129
+ value = type([:array, element, length, stride])
130
+ decorate(value, :ArrayStride, stride) if stride
131
+ value
132
+ end
133
+
134
+ # @return [Type] struct with optional member offsets and Block decoration
135
+ def struct(*members, offsets: nil, block: false)
136
+ raise Error, "one offset per struct member is required" if offsets && offsets.length != members.length
137
+ value = type([:struct, members.freeze, nil, offsets&.freeze, block])
138
+ decorate(value, :Block) if block
139
+ offsets&.each_with_index { |offset, index| member_decorate(value, index, :Offset, offset) }
140
+ value
141
+ end
142
+
143
+ # Intern a scalar or nested composite constant by its exact bit pattern.
144
+ # @return [Value] module-scoped constant (not a mutable variable)
145
+ def constant(description, value)
146
+ value_type = type(description)
147
+ opcode, args = constant_encoding(value_type, value)
148
+ key = [value_type.id, opcode, *args]
149
+ @constants[key] ||= begin
150
+ id = reserve_id
151
+ emit(:declarations, opcode, value_type.id, id, *args)
152
+ Value.new(self, nil, value_type, id)
153
+ end
154
+ end
155
+
156
+ # @yieldparam function [Function] fragment shader builder
157
+ # @return [Function] completed entry point
158
+ def fragment_shader(name = "main", &block) = define_shader(:Fragment, name, &block)
159
+ # @yieldparam function [Function] vertex shader builder
160
+ # @return [Function] completed entry point
161
+ def vertex_shader(name = "main", &block) = define_shader(:Vertex, name, &block)
162
+
163
+ # Serialize sections in specification order and reject unresolved IDs.
164
+ # @return [String] little-endian SPIR-V bytes; retain for repeated GPU use
165
+ def to_binary
166
+ raise Error, "unfinished shader" unless @functions.all?(&:finished?)
167
+ missing = (1...@next_id).reject { |id| @definitions[id] }
168
+ raise Error, "unresolved ids: #{missing.join(', ')}" unless missing.empty?
169
+ binary = [Binary::MAGIC, @version, @generator, @next_id, 0, *SECTIONS.flat_map { |section| @sections.fetch(section) }].pack("V*")
170
+ Binary.disassemble(binary)
171
+ binary
172
+ end
173
+
174
+ # Extract an ID after checking module ownership.
175
+ # @return [Integer]
176
+ def identifier(value)
177
+ if value.respond_to?(:owner)
178
+ raise Error, "value belongs to another module" unless value.owner.equal?(self)
179
+ end
180
+ value.respond_to?(:id) ? value.id : value
181
+ end
182
+
183
+ private
184
+
185
+ def expand_type_alias(key)
186
+ return unless key.is_a?(Symbol)
187
+ if (match = key.to_s.match(/\A([biu]?)vec([2-4])\z/))
188
+ [:vector, {"" => :float, "b" => :bool, "i" => :int, "u" => :uint}.fetch(match[1]), match[2].to_i]
189
+ elsif (match = key.to_s.match(/\Amat([2-4])(?:x([2-4]))?\z/))
190
+ [:matrix, "vec#{match[2] || match[1]}".to_sym, match[1].to_i]
191
+ end
192
+ end
193
+
194
+ def define_type(key, description)
195
+ element = nil
196
+ length = nil
197
+ members = []
198
+ case key
199
+ when :void then kind, opcode, args = :void, "OpTypeVoid", []
200
+ when :bool then kind, opcode, args = :bool, "OpTypeBool", []
201
+ when :float then kind, opcode, args = :float, "OpTypeFloat", [32]
202
+ when :int then kind, opcode, args = :int, "OpTypeInt", [32, 1]
203
+ when :uint then kind, opcode, args = :uint, "OpTypeInt", [32, 0]
204
+ else
205
+ raise Error, "unknown type #{description.inspect}" unless key.is_a?(Array)
206
+ kind = key[0]
207
+ case kind
208
+ when :vector, :matrix
209
+ element, length = type(key[1]), key[2]
210
+ raise Error, "vector/matrix dimensions must be 2..4" unless length.is_a?(Integer) && length.between?(2, 4)
211
+ raise Error, "invalid vector scalar" if kind == :vector && !%i[float int uint bool].include?(element.kind)
212
+ raise Error, "matrix needs float column vectors" if kind == :matrix && !(element.kind == :vector && element.element.kind == :float)
213
+ capability(:Matrix) if kind == :matrix
214
+ opcode, args = kind == :vector ? ["OpTypeVector", [element.id, length]] : ["OpTypeMatrix", [element.id, length]]
215
+ when :pointer
216
+ element = type(key[2])
217
+ length = key[1]
218
+ opcode, args = "OpTypePointer", [key[1], element.id]
219
+ when :function
220
+ members = key.drop(1).map { |item| type(item) }
221
+ opcode, args = "OpTypeFunction", members.map(&:id)
222
+ when :image2d
223
+ element = type(:float)
224
+ opcode, args = "OpTypeImage", [element.id, :"2D", 0, 0, 0, 1, :Unknown]
225
+ when :sampled_image
226
+ element = type([:image2d])
227
+ opcode, args = "OpTypeSampledImage", [element.id]
228
+ when :array
229
+ element, length = type(key[1]), key[2]
230
+ raise Error, "array length must be positive" unless length.is_a?(Integer) && length.between?(1, 0xffffffff)
231
+ opcode, args = "OpTypeArray", [element.id, constant(:uint, length).id]
232
+ when :struct
233
+ members = key[1].map { |item| type(item) }
234
+ opcode, args = "OpTypeStruct", members.map(&:id)
235
+ else
236
+ raise Error, "unknown type #{description.inspect}"
237
+ end
238
+ end
239
+ # Aliases, pointers and composites all converge on structural IDs.
240
+ canonical = [opcode, *args, *(key.is_a?(Array) && [:array, :struct].include?(kind) ? key.drop(3) : [])].freeze
241
+ return @types[key] = @types[canonical] if @types.key?(canonical)
242
+ id = reserve_id
243
+ emit(:declarations, opcode, id, *args)
244
+ value = Type.new(id:, kind:, element:, length:, members: members.freeze, owner: self)
245
+ @types[key] = @types[canonical] = value
246
+ end
247
+
248
+ def constant_encoding(value_type, value)
249
+ case value_type.kind
250
+ when :bool
251
+ raise Error, "boolean constant must be true or false" unless value == true || value == false
252
+ opcode, args = value ? ["OpConstantTrue", []] : ["OpConstantFalse", []]
253
+ when :float
254
+ raise Error, "float constant must be Numeric" unless value.is_a?(Numeric)
255
+ opcode, args = "OpConstant", [Binary.float_bits(value)]
256
+ when :int, :uint
257
+ range = value_type.kind == :int ? (-0x80000000..0x7fffffff) : (0..0xffffffff)
258
+ raise Error, "integer constant out of range" unless value.is_a?(Integer) && range.cover?(value)
259
+ opcode, args = "OpConstant", [value & 0xffffffff]
260
+ when :vector, :matrix, :array, :struct
261
+ expected = value_type.kind == :struct ? value_type.members.length : value_type.length
262
+ raise Error, "expected #{expected} composite elements" unless value.is_a?(Array) && value.length == expected
263
+ args = value.each_with_index.map do |item, index|
264
+ component_type = value_type.kind == :struct ? value_type.members[index] : value_type.element
265
+ constant(component_type, item).id
266
+ end
267
+ opcode = "OpConstantComposite"
268
+ else
269
+ raise Error, "type cannot be a constant"
270
+ end
271
+ [opcode, args]
272
+ end
273
+
274
+ def apply_decoration(opcode, id, decoration, values, member = nil)
275
+ number = Binary.enum("Decoration", decoration)[0]
276
+ key = [id, member, number]
277
+ raise Error, "conflicting #{decoration} decoration" if @decorations.key?(key) && @decorations[key] != values
278
+ unless @decorations.key?(key)
279
+ emit(:annotations, opcode, id, *[member].compact, decoration, *values)
280
+ @decorations[key] = values.freeze
281
+ end
282
+ self
283
+ end
284
+
285
+ def define_shader(stage, entry_name)
286
+ raise Error, "shader block required" unless block_given?
287
+ raise Error, "duplicate entry point #{entry_name}" if @entries[[stage, entry_name]]
288
+ function = Function.new(self, stage)
289
+ @functions << function
290
+ yield function
291
+ function.finish
292
+ @entries[[stage, entry_name]] = function.id
293
+ emit(:entry_points, "OpEntryPoint", stage, function.id, entry_name, *function.interfaces)
294
+ emit(:execution_modes, "OpExecutionMode", function.id, :OriginUpperLeft) if stage == :Fragment
295
+ name(function.id, entry_name)
296
+ function
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Electra
4
+ # Immutable structural type metadata; IDs are local to its owner module.
5
+ Type = Data.define(:id, :kind, :element, :length, :members, :owner) do
6
+ # @return [Integer] numeric SPIR-V type identifier
7
+ def to_i = id
8
+ # @return [Type] vector element type, or self for other types
9
+ def scalar = kind == :vector ? element : self
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Electra
4
+ # Immutable SSA value or typed variable pointer, tied to one module/function.
5
+ class Value
6
+ attr_reader :owner, :function, :type, :id, :storage
7
+
8
+ def initialize(owner, function, type, id, storage = nil)
9
+ @owner, @function, @type, @id, @storage = owner, function, type, id, storage
10
+ freeze
11
+ end
12
+
13
+ # @return [Integer] numeric SPIR-V identifier
14
+ def to_i = id
15
+ # @return [Value] loaded SSA value, or self for an existing SSA value
16
+ def load
17
+ return self unless storage
18
+ function.result("OpLoad", type, id)
19
+ end
20
+
21
+ # Store a type-compatible value into a writable variable.
22
+ # @return [self]
23
+ def store(value)
24
+ raise Error, "cannot store to #{storage || 'an SSA value'}" unless %i[Output Function Private].include?(storage)
25
+ item = function.value(value, type)
26
+ function.emit("OpStore", id, item.id)
27
+ self
28
+ end
29
+
30
+ # @param index [Integer, Symbol] vector index or rgba/xyzw component
31
+ # @return [Value] extracted scalar
32
+ def component(index)
33
+ raise Error, "component needs a function-local value" unless function
34
+ index = {r: 0, g: 1, b: 2, a: 3, x: 0, y: 1, z: 2, w: 3}.fetch(index, index)
35
+ raise Error, "component index out of bounds" unless type.kind == :vector && index.is_a?(Integer) && index.between?(0, type.length - 1)
36
+ function.result("OpCompositeExtract", type.element, load.id, index)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Electra
4
+ # Library semantic version (independent from SPIR-V module versions).
5
+ VERSION = "0.1.0"
6
+ end
data/lib/electra.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "electra/version"
4
+ require_relative "electra/data_compat"
5
+
6
+ # Pure Ruby construction and inspection of UI vertex/fragment SPIR-V modules.
7
+ module Electra
8
+ # Invalid shader construction, malformed binary or unsupported assembly.
9
+ class Error < StandardError; end
10
+
11
+ # Decode a SPIR-V module using the generated Khronos operand grammar.
12
+ # @param binary [String] complete SPIR-V bytes, little or big endian
13
+ # @return [String] numeric-ID assembly, including round-trip header comments
14
+ # @raise [Error] on malformed header, framing, strings, operands or ID bounds
15
+ def self.disassemble(binary)
16
+ Binary.disassemble(binary)
17
+ end
18
+
19
+ # Assemble numeric-ID SPIR-V text without invoking an external compiler.
20
+ # @param source [String] assembly in the format returned by {.disassemble}
21
+ # @return [String] binary bytes
22
+ # @raise [Error] on malformed or unsupported assembly
23
+ def self.assemble(source)
24
+ Binary.assemble(source)
25
+ end
26
+ end
27
+
28
+ require_relative "electra/binary"
29
+ require_relative "electra/type"
30
+ require_relative "electra/module"
31
+ require_relative "electra/value"
32
+ require_relative "electra/function"
data/sig/electra.rbs ADDED
@@ -0,0 +1,105 @@
1
+ module Electra
2
+ VERSION: String
3
+ type type_description = Symbol | String | Type | Array[untyped]
4
+ type branch = ^(Function) -> untyped
5
+
6
+ class Error < StandardError
7
+ end
8
+
9
+ def self.disassemble: (String binary) -> String
10
+ def self.assemble: (String source) -> String
11
+
12
+ class Type < Data
13
+ attr_reader id: Integer
14
+ attr_reader kind: Symbol
15
+ attr_reader element: Type?
16
+ attr_reader length: (Integer | Symbol)?
17
+ attr_reader members: Array[Type]
18
+ attr_reader owner: Module
19
+ def to_i: () -> Integer
20
+ def scalar: () -> Type
21
+ end
22
+
23
+ class Module
24
+ attr_reader version: Integer
25
+ def initialize: (?version: String, ?generator: Integer) -> void
26
+ def reserve_id: (?untyped name) -> Integer
27
+ alias ref reserve_id
28
+ def emit: (Symbol section, String | Symbol opcode, *untyped operands) -> self
29
+ def capability: (String | Symbol | Integer name) -> self
30
+ def extension: (String name) -> self
31
+ def import: (String name) -> Integer
32
+ def memory_model: (?String | Symbol addressing, ?String | Symbol model) -> self
33
+ def name: (Integer | Type | Value target, String text) -> self
34
+ def decorate: (Integer | Type | Value target, String | Symbol decoration, *untyped values) -> self
35
+ def member_decorate: (Integer | Type | Value target, Integer member, String | Symbol decoration, *untyped values) -> self
36
+ def type: (type_description description) -> Type
37
+ def array: (type_description element, Integer length, ?stride: Integer?) -> Type
38
+ def struct: (*type_description members, ?offsets: Array[Integer]?, ?block: bool) -> Type
39
+ def constant: (type_description description, untyped value) -> Value
40
+ def fragment_shader: (?String name) { (Function) -> untyped } -> Function
41
+ def vertex_shader: (?String name) { (Function) -> untyped } -> Function
42
+ def to_binary: () -> String
43
+ def identifier: (Integer | Type | Value value) -> Integer
44
+ end
45
+
46
+ class Value
47
+ attr_reader owner: Module
48
+ attr_reader function: Function?
49
+ attr_reader type: Type
50
+ attr_reader id: Integer
51
+ attr_reader storage: Symbol?
52
+ def initialize: (Module owner, Function? function, Type type, Integer id, ?Symbol? storage) -> void
53
+ def to_i: () -> Integer
54
+ def load: () -> Value
55
+ def store: (untyped value) -> self
56
+ def component: (Integer | Symbol index) -> Value
57
+ end
58
+
59
+ class Function
60
+ attr_reader owner: Module
61
+ attr_reader id: Integer
62
+ attr_reader interfaces: Array[Integer]
63
+ attr_reader stage: Symbol
64
+ def initialize: (Module owner, Symbol stage) -> void
65
+ def input: (type_description type, ?location: Integer?, ?builtin: Symbol?, ?flat: bool) -> Value
66
+ def output: (type_description type, ?location: Integer?, ?builtin: Symbol?) -> Value
67
+ def sampled_image_2d: (?set: Integer, ?binding: Integer) -> Value
68
+ def uniform_buffer: (*type_description types, ?set: Integer, ?binding: Integer, offsets: Array[Integer]) -> Value
69
+ def push_constant: (*type_description types, offsets: Array[Integer]) -> Value
70
+ def member: (Value buffer, Integer index) -> Value
71
+ def constant: (untyped item, ?type_description? type) -> Value
72
+ def value: (untyped item, ?type_description? expected) -> Value
73
+ def construct: (type_description description, *untyped items) -> Value
74
+ def splat: (untyped item, ?Integer count) -> Value
75
+ def sample: (Value texture, untyped uv, ?lod: untyped) -> Value
76
+ def add: (untyped left, untyped right) -> Value
77
+ def sub: (untyped left, untyped right) -> Value
78
+ def div: (untyped left, untyped right) -> Value
79
+ def mod: (untyped left, untyped right) -> Value
80
+ def mul: (untyped left, untyped right) -> Value
81
+ def negate: (untyped item) -> Value
82
+ def dot: (untyped left, untyped right) -> Value
83
+ def transpose: (untyped item) -> Value
84
+ def convert: (untyped item, type_description description) -> Value
85
+ def equal: (untyped left, untyped right) -> Value
86
+ def not_equal: (untyped left, untyped right) -> Value
87
+ def less_than: (untyped left, untyped right) -> Value
88
+ def less_equal: (untyped left, untyped right) -> Value
89
+ def greater_than: (untyped left, untyped right) -> Value
90
+ def greater_equal: (untyped left, untyped right) -> Value
91
+ def logical_and: (untyped left, untyped right) -> Value
92
+ def logical_or: (untyped left, untyped right) -> Value
93
+ def logical_not: (untyped item) -> Value
94
+ def select: (untyped condition, untyped yes, untyped no) -> Value
95
+ def if_else: (untyped condition, branch yes, ?branch? no) -> Value?
96
+ def discard: () -> nil
97
+ def discard_if: (untyped condition) -> nil
98
+ def return_void: () -> nil
99
+ def ext: (Symbol operation, *untyped items) -> Value
100
+ def emit: (String opcode, *untyped operands) -> self
101
+ def result: (String opcode, type_description type, *untyped operands) -> Value
102
+ def finish: () -> self
103
+ def finished?: () -> bool
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: electra
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ email:
13
+ - t.yudai92@gmail.com
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - CHANGELOG.md
19
+ - LICENSE-SPIRV-Headers.txt
20
+ - LICENSE.txt
21
+ - README.md
22
+ - examples/example_shaders.rb
23
+ - lib/electra.rb
24
+ - lib/electra/binary.rb
25
+ - lib/electra/data_compat.rb
26
+ - lib/electra/function.rb
27
+ - lib/electra/grammar.rb
28
+ - lib/electra/module.rb
29
+ - lib/electra/type.rb
30
+ - lib/electra/value.rb
31
+ - lib/electra/version.rb
32
+ - sig/electra.rbs
33
+ homepage: https://github.com/noxdea/electra
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ source_code_uri: https://github.com/noxdea/electra
38
+ changelog_uri: https://github.com/noxdea/electra/blob/main/CHANGELOG.md
39
+ allowed_push_host: https://rubygems.org
40
+ rubygems_mfa_required: 'true'
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '3.1'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 4.0.19
56
+ specification_version: 4
57
+ summary: A pure Ruby SPIR-V shader emitter and disassembler
58
+ test_files: []