glslkit 0.1.0.pre

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,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "diagnostic"
4
+
5
+ module Glslkit
6
+ # Glslkit::Program(§8.3)を1つ受け取り、単一ステージで完結する検証ルールを
7
+ # 実行する。マージ前のReflectionを直接見るため、Manifestが統合してしまう
8
+ # 情報(同一ステージ内の重複など)も検出できる。例外は投げず、診断の配列を
9
+ # 常に返す。
10
+ class Validator
11
+ class Result
12
+ def initialize(diagnostics)
13
+ @diagnostics = diagnostics
14
+ end
15
+
16
+ attr_reader :diagnostics
17
+
18
+ def errors
19
+ diagnostics.select(&:error?)
20
+ end
21
+
22
+ def warnings
23
+ diagnostics.select(&:warning?)
24
+ end
25
+
26
+ def ok?
27
+ errors.empty?
28
+ end
29
+ end
30
+
31
+ def initialize(disabled: [])
32
+ @disabled = disabled.map(&:to_s)
33
+ end
34
+
35
+ def validate(program)
36
+ diagnostics = []
37
+ diagnostics.concat(check_out_in_correspondence(program))
38
+ diagnostics.concat(check_cross_stage_uniform_mismatch(program)) if enabled?("E003")
39
+ diagnostics.concat(check_attribute_location_collisions(program)) if enabled?("E004")
40
+ diagnostics.concat(check_output_location_collisions(program)) if enabled?("E005")
41
+ diagnostics.concat(check_duplicate_names_within_a_stage(program)) if enabled?("E006")
42
+ diagnostics.concat(check_reserved_gl_prefix(program)) if enabled?("E007")
43
+ Result.new(diagnostics)
44
+ end
45
+
46
+ private
47
+
48
+ # E001/E002/W001はvertexのoutとfragmentのin(Reflection上はどちらも
49
+ # `attributes`/`outputs`という同じ構造体で表現される)を名前で突き合わせる。
50
+ # 3つとも両ステージが揃っていないと意味を持たないため、片方でも欠けていれば
51
+ # 何も検出しない(§8.3 E002のスキップ条件と同じ扱いをW001/E001にも広げる)。
52
+ #
53
+ # アンカーはSPEC.md §8.3の規則通り: fragment側に該当宣言があればfragment、
54
+ # 無ければvertexを指す。E001は必ず両側に宣言があるのでfragment固定、
55
+ # E002はfragment側にのみ、W001はvertex側にのみ宣言がある。
56
+ def check_out_in_correspondence(program)
57
+ vertex = program.sources[:vertex]
58
+ fragment = program.sources[:fragment]
59
+ return [] unless vertex && fragment
60
+
61
+ vertex_out = index_by_name(vertex.reflection.outputs)
62
+ fragment_in = index_by_name(fragment.reflection.attributes)
63
+
64
+ diagnostics = []
65
+ diagnostics.concat(check_type_mismatch(program, fragment, vertex_out, fragment_in)) if enabled?("E001")
66
+ diagnostics.concat(check_missing_vertex_out(program, fragment, vertex_out, fragment_in)) if enabled?("E002")
67
+ diagnostics.concat(check_missing_fragment_in(program, vertex, vertex_out, fragment_in)) if enabled?("W001")
68
+ diagnostics
69
+ end
70
+
71
+ def check_type_mismatch(program, fragment, vertex_out, fragment_in)
72
+ (vertex_out.keys & fragment_in.keys).filter_map do |name|
73
+ v, f = vertex_out[name], fragment_in[name]
74
+ next if v.type == f.type
75
+
76
+ message = "\"#{name}\": vertex out is #{v.type}, fragment in is #{f.type}"
77
+ build_diagnostic(program, :fragment, fragment, "E001", name, f.output_line, message)
78
+ end
79
+ end
80
+
81
+ def check_missing_vertex_out(program, fragment, vertex_out, fragment_in)
82
+ (fragment_in.keys - vertex_out.keys).map do |name|
83
+ f = fragment_in[name]
84
+ message = "\"#{name}\": fragment in has no matching vertex out"
85
+ build_diagnostic(program, :fragment, fragment, "E002", name, f.output_line, message, severity: :warning)
86
+ end
87
+ end
88
+
89
+ def check_missing_fragment_in(program, vertex, vertex_out, fragment_in)
90
+ (vertex_out.keys - fragment_in.keys).map do |name|
91
+ v = vertex_out[name]
92
+ message = "\"#{name}\": vertex out has no matching fragment in"
93
+ build_diagnostic(program, :vertex, vertex, "W001", name, v.output_line, message, severity: :warning)
94
+ end
95
+ end
96
+
97
+ # 既存のManifest#merge_uniforms/#merge_uniform_blocksがStageMismatchErrorを
98
+ # 投げる条件を、マージ前に同じロジックで検出する(§8.4「E003の扱い」)。
99
+ # 名前が両ステージに存在する場合のみ比較する(片方だけならエラーにならない、
100
+ # という既存のManifestの挙動に合わせる)。
101
+ def check_cross_stage_uniform_mismatch(program)
102
+ vertex = program.sources[:vertex]
103
+ fragment = program.sources[:fragment]
104
+ return [] unless vertex && fragment
105
+
106
+ check_uniform_type_mismatch(program, vertex, fragment) +
107
+ check_uniform_block_mismatch(program, vertex, fragment)
108
+ end
109
+
110
+ def check_uniform_type_mismatch(program, vertex, fragment)
111
+ vertex_uniforms = index_by_name(vertex.reflection.uniforms)
112
+ fragment_uniforms = index_by_name(fragment.reflection.uniforms)
113
+
114
+ (vertex_uniforms.keys & fragment_uniforms.keys).filter_map do |name|
115
+ v, f = vertex_uniforms[name], fragment_uniforms[name]
116
+ next if v.type == f.type
117
+
118
+ message = "uniform \"#{name}\" is #{v.type} in vertex but #{f.type} in fragment"
119
+ build_diagnostic(program, :fragment, fragment, "E003", name, f.output_line, message)
120
+ end
121
+ end
122
+
123
+ def check_uniform_block_mismatch(program, vertex, fragment)
124
+ vertex_blocks = index_by_name(vertex.reflection.uniform_blocks)
125
+ fragment_blocks = index_by_name(fragment.reflection.uniform_blocks)
126
+
127
+ (vertex_blocks.keys & fragment_blocks.keys).filter_map do |name|
128
+ v, f = vertex_blocks[name], fragment_blocks[name]
129
+ next if v.layout == f.layout && v.binding == f.binding
130
+
131
+ message = "uniform block \"#{name}\" differs between vertex and fragment " \
132
+ "(layout: #{v.layout} vs #{f.layout}, binding: #{v.binding.inspect} vs #{f.binding.inspect})"
133
+ build_diagnostic(program, :fragment, fragment, "E003", name, f.output_line, message)
134
+ end
135
+ end
136
+
137
+ def index_by_name(entries)
138
+ entries.each_with_object({}) { |entry, hash| hash[entry.name] = entry }
139
+ end
140
+
141
+ def enabled?(code)
142
+ !@disabled.include?(code)
143
+ end
144
+
145
+ # attributeはvertexステージにしか存在しない(SPEC.md §4のManifest決定)。
146
+ def check_attribute_location_collisions(program)
147
+ stage = :vertex
148
+ source = program.sources[stage]
149
+ return [] unless source
150
+
151
+ duplicate_groups_by(source.reflection.attributes, &:location).flat_map do |location, entries|
152
+ message = "attribute location #{location} is used by #{entries.map(&:name).join(", ")}"
153
+ [group_diagnostic(program, stage, source, "E004", entries, message)]
154
+ end
155
+ end
156
+
157
+ # outputはfragmentステージにしか存在しない(同上)。
158
+ def check_output_location_collisions(program)
159
+ stage = :fragment
160
+ source = program.sources[stage]
161
+ return [] unless source
162
+
163
+ duplicate_groups_by(source.reflection.outputs, &:location).flat_map do |location, entries|
164
+ message = "output location #{location} is used by #{entries.map(&:name).join(", ")}"
165
+ [group_diagnostic(program, stage, source, "E005", entries, message)]
166
+ end
167
+ end
168
+
169
+ # GLSLの識別子はattribute/uniform/uniform block/outputを問わず1つの
170
+ # 名前空間を共有するため、種類をまたいで同名かどうかをチェックする。
171
+ def check_duplicate_names_within_a_stage(program)
172
+ Program::STAGES.flat_map do |stage|
173
+ source = program.sources[stage]
174
+ next [] unless source
175
+
176
+ duplicate_groups_by(all_declarations(source.reflection), &:name).flat_map do |name, entries|
177
+ [group_diagnostic(program, stage, source, "E006", entries, "\"#{name}\" is declared more than once")]
178
+ end
179
+ end
180
+ end
181
+
182
+ def check_reserved_gl_prefix(program)
183
+ Program::STAGES.flat_map do |stage|
184
+ source = program.sources[stage]
185
+ next [] unless source
186
+
187
+ all_declarations(source.reflection).select { |entry| entry.name.start_with?("gl_") }.map do |entry|
188
+ message = "\"#{entry.name}\" starts with the reserved prefix \"gl_\""
189
+ build_diagnostic(program, stage, source, "E007", entry.name, entry.output_line, message)
190
+ end
191
+ end
192
+ end
193
+
194
+ def all_declarations(reflection)
195
+ reflection.attributes + reflection.uniforms + reflection.uniform_blocks + reflection.outputs
196
+ end
197
+
198
+ # yieldした値がnilのエントリは無視する(未指定location同士を衝突扱いに
199
+ # しないため)。出現順を保ったまま、2件以上あるグループだけを返す。
200
+ def duplicate_groups_by(entries)
201
+ groups = Hash.new { |h, k| h[k] = [] }
202
+ entries.each do |entry|
203
+ key = yield entry
204
+ groups[key] << entry unless key.nil?
205
+ end
206
+ groups.select { |_key, group| group.size > 1 }
207
+ end
208
+
209
+ def group_diagnostic(program, stage, source, code, entries, message)
210
+ anchor = entries.min_by(&:output_line)
211
+ build_diagnostic(program, stage, source, code, nil, anchor.output_line, message)
212
+ end
213
+
214
+ def build_diagnostic(program, stage, source, code, name, output_line, message, severity: :error)
215
+ file, line = source.source_map&.resolve(output_line) || [nil, nil]
216
+ Diagnostic.new(
217
+ severity: severity,
218
+ code: code,
219
+ message: message,
220
+ program: program.name,
221
+ stage: stage,
222
+ name: name,
223
+ file: file,
224
+ line: line
225
+ )
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ VERSION = "0.1.0.pre"
5
+ end
data/lib/glslkit.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "glslkit/version"
4
+ require_relative "glslkit/errors"
5
+ require_relative "glslkit/types"
6
+ require_relative "glslkit/resolver"
7
+ require_relative "glslkit/resolvers/file_system"
8
+ require_relative "glslkit/resolvers/hash"
9
+ require_relative "glslkit/source_map"
10
+ require_relative "glslkit/source"
11
+ require_relative "glslkit/reflection"
12
+ require_relative "glslkit/preprocessor"
13
+ require_relative "glslkit/manifest"
14
+ require_relative "glslkit/minifier"
15
+ require_relative "glslkit/program"
16
+ require_relative "glslkit/diagnostic"
17
+ require_relative "glslkit/validator"
18
+ require_relative "glslkit/bundle"
19
+
20
+ module Glslkit
21
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glslkit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - kyubey1228
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-08-24 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Flattens #include directives in GLSL shaders and extracts uniform/attribute/output
13
+ reflection data as JSON, with zero runtime dependencies so it can run under ruby.wasm.'
14
+ email:
15
+ - kyuuka1228@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - glslkit.gemspec
24
+ - lib/glslkit.rb
25
+ - lib/glslkit/bundle.rb
26
+ - lib/glslkit/diagnostic.rb
27
+ - lib/glslkit/digest.rb
28
+ - lib/glslkit/errors.rb
29
+ - lib/glslkit/manifest.rb
30
+ - lib/glslkit/minifier.rb
31
+ - lib/glslkit/preprocessor.rb
32
+ - lib/glslkit/program.rb
33
+ - lib/glslkit/reflection.rb
34
+ - lib/glslkit/resolver.rb
35
+ - lib/glslkit/resolvers/file_system.rb
36
+ - lib/glslkit/resolvers/hash.rb
37
+ - lib/glslkit/runtime.rb
38
+ - lib/glslkit/source.rb
39
+ - lib/glslkit/source_map.rb
40
+ - lib/glslkit/types.rb
41
+ - lib/glslkit/validator.rb
42
+ - lib/glslkit/version.rb
43
+ homepage: https://github.com/kyubey1228/glslkit
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://github.com/kyubey1228/glslkit
48
+ source_code_uri: https://github.com/kyubey1228/glslkit/tree/main/core
49
+ changelog_uri: https://github.com/kyubey1228/glslkit/blob/main/core/CHANGELOG.md
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.1'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.6.2
65
+ specification_version: 4
66
+ summary: GLSL preprocessing and reflection toolkit
67
+ test_files: []