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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5a00586e597777dd9f5a4cef9aa13d7099caeef3dcbc7157616d873da0dd7092
4
+ data.tar.gz: 76cd0df41a5da9cc8ca68d349dc56f9b9abb9d951771e167d00fe35f599052bf
5
+ SHA512:
6
+ metadata.gz: d577fe840555f89644923fd758f3997d05e381f4c2887addc0af1e2ba6c4b07991fda7277e7b3b3c0e2e3173e933f00f2ea8a4b603941d0555e0cffe226550bb
7
+ data.tar.gz: 0f3f31f30b13ac2fdcdf15fcda901c191b808990d981b0005dffd5946f8fec4bb1665ce194b7b86df053eed5f5205cb93c6b2238dd84235842c301b0db89c440
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-11
4
+
5
+ - Initial release.
@@ -0,0 +1,26 @@
1
+ Files: grammar data derived from KhronosGroup/SPIRV-Headers
2
+ Copyright (c) 2015-2024 The Khronos Group Inc.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a
5
+ copy of this software and/or associated documentation files (the
6
+ "Materials"), to deal in the Materials without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Materials, and to
9
+ permit persons to whom the Materials are furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included
13
+ in all copies or substantial portions of the Materials.
14
+
15
+ MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
16
+ KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
17
+ SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
18
+ https://www.khronos.org/registry/
19
+
20
+ THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26
+ MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Electra
2
+
3
+ A pure Ruby SPIR-V shader emitter, typed vertex/fragment DSL, assembler and disassembler.
4
+
5
+ Electra builds the small shaders a UI renderer needs directly from Ruby, without running GLSL compilers or loading native bindings. Use it when a Vulkan application needs to ship shader construction as Ruby source. It is not a GLSL compiler, optimizer, or complete SPIR-V semantic validator.
6
+
7
+ Requires Ruby 3.1+. No runtime gem dependencies; the library also works with `ruby --disable-gems`. SPIRV-Tools are optional development oracles, never runtime requirements.
8
+
9
+ ## Try it
10
+
11
+ ```ruby
12
+ require "electra"
13
+ mod = Electra::Module.new
14
+ mod.fragment_shader { |f| f.output(:vec4, location: 0).store(f.constant([1, 0, 0, 1])) }
15
+ binary = mod.to_binary
16
+ puts Electra.disassemble(binary)
17
+ ```
18
+
19
+ Install the gem:
20
+
21
+ ```sh
22
+ gem install electra
23
+ ```
24
+
25
+ Or build it from this checkout:
26
+
27
+ ```sh
28
+ gem build electra.gemspec
29
+ gem install --local electra-0.1.0.gem
30
+ ruby -Ilib examples/example_shaders.rb tmp/shaders
31
+ ```
32
+
33
+ The example emits a no-vertex-buffer triangle pair and a text fragment shader. `Electra::ExampleShaders.triangle_vertex` uses `VertexIndex`, so the consumer can issue `vkCmdDraw(3, 1, 0, 0)`. Both entry points are named `main`; the fragment output is location 0.
34
+
35
+ ## Text shader
36
+
37
+ ```ruby
38
+ mod = Electra::Module.new(version: "1.0", generator: 0)
39
+ mod.capability :Shader
40
+ mod.memory_model :Logical, :GLSL450
41
+
42
+ mod.fragment_shader("main") do |f|
43
+ uv = f.input(:vec2, location: 0)
44
+ tint = f.input(:vec4, location: 1)
45
+ tex = f.sampled_image_2d(set: 0, binding: 0)
46
+ output = f.output(:vec4, location: 0)
47
+ alpha = f.sample(tex, uv).component(:r)
48
+ output.store(f.mul(tint, f.splat(alpha)))
49
+ end
50
+
51
+ binary = mod.to_binary
52
+ raise "round trip failed" unless Electra.assemble(Electra.disassemble(binary)) == binary
53
+ ```
54
+
55
+ `Shader`, `Logical/GLSL450`, `OriginUpperLeft` for fragments, and function return instructions are added automatically. Calling `capability` again is harmless. `to_binary` returns a little-endian binary String, with an exact ID bound and schema zero. Versions 1.0–1.6 are accepted; the application must select a version supported by its Vulkan environment. SPIR-V 1.4+ descriptor/push-constant variables are included in entry-point interfaces.
56
+
57
+ ## Types and operations
58
+
59
+ Scalar types are `:bool`, `:int`, `:uint`, `:float` (32-bit numbers). Vectors are `:vec2`–`:vec4`, and `:ivecN`, `:uvecN`, `:bvecN`. Matrices `:mat2`–`:mat4` and `:matCxR` are column-major floating-point matrices. `mod.array(type, count, stride:)` and `mod.struct(*types, offsets:, block:)` expose explicit aggregate layout. Struct/matrix constants use nested arrays, one array per column.
60
+
61
+ Types are structurally interned; constants are interned by their type and exact bit pattern, including negative zero. `f.constant(value, type)` creates a typed constant. Numeric literals passed alongside a typed operand use its type; use `1.0` for an inferred float and `1` for an inferred signed integer.
62
+
63
+ | Operation | API |
64
+ | --- | --- |
65
+ | Inputs/outputs | `input(type, location:)`, `output(type, location:)`; alternatively `builtin: :Position` / `:VertexIndex`; input `flat: true` |
66
+ | Components/composites | `value.component(:r)` / `component(0)`, `construct(type, *components)`, `splat(scalar, count = 4)` |
67
+ | Arithmetic | `add`, `sub`, `mul`, `div`, `mod`, `negate`; scalar/vector numeric types |
68
+ | Matrices | `mul` selects matrix×matrix, matrix×vector, vector×matrix or scalar multiplication; `transpose`, `dot` |
69
+ | Conversion | `convert(value, type)` for numeric scalar/vector conversion; int/uint conversion is bit-preserving |
70
+ | Comparison | `equal`, `not_equal`, `less_than`, `less_equal`, `greater_than`, `greater_equal` |
71
+ | Boolean/selection | `logical_and`, `logical_or`, `logical_not`, `select(condition, yes, no)` |
72
+ | Texture | `sampled_image_2d(set:, binding:)`, `sample(texture, uv, lod: nil)`; explicit LOD required outside fragments |
73
+ | Control flow | `if_else(condition, then_callback, else_callback = nil)`, `discard`, `discard_if`, `return_void` |
74
+ | GLSL.std.450 | `ext(:clamp, value, lower, upper)`, plus round/abs/floor/ceil/fract/sin/cos/pow/exp/log/sqrt/inverse_sqrt/min/max/mix/step/smoothstep/length/normalize |
75
+
76
+ Inputs and descriptors load automatically when consumed; output variables use `store`. Types and values cannot cross module boundaries, and function-local values cannot cross function boundaries. Integer fragment inputs should be declared `flat: true` as required by Vulkan.
77
+
78
+ Structured branches allocate forward labels and merge matching returned values with `OpPhi`:
79
+
80
+ ```ruby
81
+ alpha = f.input(:float, location: 0)
82
+ f.discard_if(f.less_than(alpha, 0.01))
83
+ adjusted = f.if_else(f.less_than(alpha, 0.5),
84
+ ->(branch) { branch.mul(alpha, 0.5) },
85
+ ->(branch) { branch.mul(alpha, 0.9) })
86
+ f.output(:vec4, location: 0).store(f.splat(adjusted))
87
+ ```
88
+
89
+ ## Uniform and push-constant layout
90
+
91
+ Offsets/strides are explicit: the emitter does not guess a CPU/GPU ABI.
92
+
93
+ ```ruby
94
+ buffer = f.uniform_buffer(:mat4, set: 0, binding: 0, offsets: [0])
95
+ mod.member_decorate(buffer.type, 0, :ColMajor)
96
+ mod.member_decorate(buffer.type, 0, :MatrixStride, 16)
97
+ position.store(f.mul(f.member(buffer, 0), input_position))
98
+ # f.push_constant(:vec4, offsets: [0]) returns a read-only struct variable.
99
+ ```
100
+
101
+ `decorate(target, decoration, *values)` supports the grammar's decorations, including Location, Binding, DescriptorSet, Block and ArrayStride. `member_decorate(type, index, decoration, *values)` handles Offset and matrix layout. Repeating the same decoration is harmless; conflicting values raise `Electra::Error`.
102
+
103
+ For instructions outside the DSL, `mod.reserve_id` / `mod.ref(:label)` reserve forward IDs, and `mod.emit(section, "OpName", id, "name")` accepts grammar-order operands. Sections are emitted in specification order: capabilities, extensions, imports, memory model, entry points, execution modes, debug, annotations, declarations, functions. Every reserved ID must be defined exactly once. The low-level API does not validate dominance, capability requirements or storage-layout rules: run `spirv-val`.
104
+
105
+ ## Assembly and limits
106
+
107
+ `Electra.disassemble(binary)` validates the header, instruction framing, operands, strings and ID bounds. `Electra.assemble(text)` accepts numeric `%123` IDs and SPIR-V instruction syntax; output from this disassembler round-trips byte-for-byte, preserving version, generator, bound and endianness through header comments. Strings use SPIR-V escaping (only quote/backslash need escaping; embedded line breaks remain literal). Floating-point hexadecimal spelling preserves 16/32/64-bit constants, signed zero and NaN payloads.
108
+
109
+ The complete opcode/enum table and GLSL.std.450 names are derived from a pinned [Khronos SPIRV-Headers revision](https://github.com/KhronosGroup/SPIRV-Headers/tree/496543121ce6419f23d6fa5d7194ba66c36212d2/include/spirv/unified1). The typed DSL intentionally covers UI vertex and fragment shaders only. Compute, tessellation, geometry, subgroups, ray tracing, GLSL parsing, optimization and GPU resource management are not implemented. The assembler/disassembler is not a general replacement for SPIRV-Tools: nonnumeric ID names, other extended-instruction symbolic vocabularies, and context-dependent 64-bit `OpSwitch` literals are outside its supported round-trip surface.
110
+
111
+ Errors are `Electra::Error`; builders should be discarded after a failed mutation. No binary is claimed valid merely because it can be disassembled. GPU limits and full shader semantics are checked by the actual Vulkan implementation and, in development, `spirv-val`.
112
+
113
+ Module/Function objects own mutable construction state; use one builder per thread. Type/Value records and generated grammar tables are frozen. Independent builders share no allocation counters or shader state.
114
+
115
+ ## Verification and performance
116
+
117
+ ```sh
118
+ bundle install
119
+ bundle exec rake # tests and isolated install
120
+ bundle exec rake test:oracle # requires SPIRV-Tools; skips if absent
121
+ BUDGET=1 bundle exec rake bench # 10ms regression guard, not a frame-time target
122
+ rbs -I sig validate
123
+ ```
124
+
125
+ The independent tests include malformed/truncated binaries, 1,000 seeded binary mutations, type/constant interning, forward references, exact assembly round trips, float boundaries and strings. When installed, `spirv-val`, `spirv-dis` and `spirv-as` check complete emitted instruction streams, including comparisons, nested branches/phi/discard, matrix operations, buffer layout, explicit LOD and GLSL intrinsics across SPIR-V 1.0/1.3/1.4/1.5/1.6. Linux CI installs these tools; macOS/Windows skip only external oracles when unavailable. A Vulkan draw/readback test belongs to the renderer consuming the shared triangle shader pair; this library's SPIRV-Tools tests alone do not prove a rendered image.
126
+
127
+ Measured locally on arm64 macOS, Ruby 4.0.0 + YJIT; 604-byte text fragment, median of five runs of 1,000 operations:
128
+
129
+ | Workload | Time | Allocated objects |
130
+ | --- | ---: | ---: |
131
+ | Build and serialize text fragment | 170.37 µs | 1,532 |
132
+ | Disassemble | 108.82 µs | 959 |
133
+ | Assemble | 205.05 µs | 1,653 |
134
+
135
+ Shaders are intended to be built once and retained, not rebuilt per frame. Rerun `bench/emitter.rb` on the target Ruby/platform; these are measurements, not universal guarantees.
136
+
137
+ ## Name and license
138
+
139
+ “Electra” is the engraving tool that cuts precise lines into metal: this library similarly engraves SPIR-V instructions. The RubyGems name is `electra`, namespace `Electra`.
140
+
141
+ MIT; see [LICENSE.txt](LICENSE.txt). Khronos grammar and derived tables retain their [upstream license](LICENSE-SPIRV-Headers.txt). Protocol references: [SPIR-V registry](https://registry.khronos.org/SPIR-V/), [Vulkan SPIR-V environment](https://docs.vulkan.org/spec/latest/appendices/spirvenv.html), [GLSL.std.450](https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html).
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "electra"
4
+
5
+ # Also shared by the Vulkan integration test. No vertex buffer: VertexIndex
6
+ # chooses three clip-space positions, so a vkCmdDraw(3, 1, 0, 0) suffices.
7
+ module Electra
8
+ module ExampleShaders
9
+ module_function
10
+
11
+ def triangle_vertex
12
+ mod = Module.new
13
+ mod.vertex_shader do |f|
14
+ index = f.input(:int, builtin: :VertexIndex)
15
+ position = f.output(:vec4, builtin: :Position)
16
+ first = f.constant([-0.75, -0.75, 0, 1], :vec4)
17
+ second = f.constant([0.75, -0.75, 0, 1], :vec4)
18
+ third = f.constant([0, 0.75, 0, 1], :vec4)
19
+ position.store(f.select(f.equal(index, 0), first, f.select(f.equal(index, 1), second, third)))
20
+ end
21
+ mod.to_binary
22
+ end
23
+
24
+ def triangle_fragment
25
+ mod = Module.new
26
+ mod.fragment_shader { |f| f.output(:vec4, location: 0).store(f.constant([1, 0.25, 0, 1], :vec4)) }
27
+ mod.to_binary
28
+ end
29
+
30
+ def text_fragment(version: "1.0")
31
+ mod = Module.new(version:)
32
+ mod.fragment_shader do |f|
33
+ uv = f.input(:vec2, location: 0)
34
+ tint = f.input(:vec4, location: 1)
35
+ tex = f.sampled_image_2d(set: 0, binding: 0)
36
+ output = f.output(:vec4, location: 0)
37
+ alpha = f.sample(tex, uv).component(:r)
38
+ output.store(f.mul(tint, f.splat(alpha)))
39
+ end
40
+ mod.to_binary
41
+ end
42
+ end
43
+ end
44
+
45
+ if $PROGRAM_NAME == __FILE__
46
+ require "fileutils"
47
+ destination = ARGV.fetch(0, "tmp/shaders")
48
+ FileUtils.mkdir_p(destination)
49
+ %i[triangle_vertex triangle_fragment text_fragment].each do |name|
50
+ binary = Electra::ExampleShaders.public_send(name)
51
+ File.binwrite(File.join(destination, "#{name}.spv"), binary)
52
+ File.write(File.join(destination, "#{name}.spvasm"), Electra.disassemble(binary))
53
+ puts "#{name}: #{binary.bytesize} bytes"
54
+ end
55
+ end