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,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require_relative "digest"
5
+ require_relative "errors"
6
+ require_relative "source"
7
+ require_relative "source_map"
8
+ require_relative "reflection"
9
+
10
+ module Glslkit
11
+ class Preprocessor
12
+ INCLUDE_PATTERN = /\A[ \t]*#[ \t]*include[ \t]*(?:"([^"]+)"|<([^>]+)>)[ \t]*\z/
13
+ VERSION_PATTERN = /\A[ \t]*#[ \t]*version\b/
14
+ EXTENSION_PATTERN = /\A[ \t]*#[ \t]*extension\b/
15
+ PRAGMA_ONCE_PATTERN = /\A[ \t]*#[ \t]*pragma[ \t]+once[ \t]*\z/
16
+
17
+ def initialize(resolver:, line_directives: true)
18
+ @resolver = resolver
19
+ @line_directives = line_directives
20
+ end
21
+
22
+ def process(entry_request)
23
+ run = Run.new
24
+ canonical_path, content = @resolver.read(entry_request, from: nil)
25
+ expand(run, canonical_path, content)
26
+
27
+ header = []
28
+ header << run.version if run.version
29
+ header.concat(run.extensions)
30
+
31
+ code = "#{(header + run.body).join("\n")}\n"
32
+
33
+ # segmentsはbody基準(ヘッダを含まない)の行番号で溜めてあるので、
34
+ # ヘッダの行数だけ一括でずらしてからSourceMapに登録する。ヘッダの
35
+ # 行数はexpand()完了まで確定しない(#versionがどのファイルで見つかる
36
+ # かは走査してみないと分からない)ため、この2段階が必要になる。
37
+ run.segments.each do |body_line, file_index, source_line|
38
+ run.source_map.add_segment(output_line: header.size + body_line, file_index: file_index, source_line: source_line)
39
+ end
40
+
41
+ Source.new(
42
+ code: code,
43
+ source_map: run.source_map,
44
+ reflection: Reflection.new(code),
45
+ digest: Digest.hexdigest(code)
46
+ )
47
+ end
48
+
49
+ private
50
+
51
+ # トップレベルの#process呼び出し1回分の間で共有される可変状態。
52
+ class Run
53
+ attr_accessor :version
54
+ attr_reader :source_map, :extensions, :body, :ancestor_stack, :segments
55
+
56
+ def initialize
57
+ @source_map = SourceMap.new
58
+ @extensions = []
59
+ @extension_set = Set.new
60
+ @pragma_once_seen = Set.new
61
+ @ancestor_stack = []
62
+ @body = []
63
+ @segments = [] # [body_relative_output_line, file_index, source_line]
64
+ end
65
+
66
+ def pragma_once?(canonical_path)
67
+ @pragma_once_seen.include?(canonical_path)
68
+ end
69
+
70
+ def mark_pragma_once(canonical_path)
71
+ @pragma_once_seen << canonical_path
72
+ end
73
+
74
+ def add_extension(value)
75
+ return if @extension_set.include?(value)
76
+
77
+ @extension_set << value
78
+ @extensions << value
79
+ end
80
+ end
81
+ private_constant :Run
82
+
83
+ def expand(run, canonical_path, content)
84
+ if run.ancestor_stack.include?(canonical_path)
85
+ chain = (run.ancestor_stack + [canonical_path]).join(" -> ")
86
+ raise CircularIncludeError, chain
87
+ end
88
+
89
+ file_index = run.source_map.index_for(canonical_path)
90
+ run.ancestor_stack.push(canonical_path)
91
+ needs_marker = true
92
+
93
+ content.each_line.with_index(1) do |raw_line, lineno|
94
+ line = raw_line.chomp
95
+
96
+ case line
97
+ when PRAGMA_ONCE_PATTERN
98
+ run.mark_pragma_once(canonical_path)
99
+ needs_marker = true
100
+ when VERSION_PATTERN
101
+ value = line.strip
102
+ if run.version && run.version != value
103
+ raise VersionConflictError, "#{run.version.inspect} vs #{value.inspect} (in #{canonical_path})"
104
+ end
105
+ run.version = value
106
+ needs_marker = true
107
+ when EXTENSION_PATTERN
108
+ run.add_extension(line.strip)
109
+ needs_marker = true
110
+ when INCLUDE_PATTERN
111
+ request = $1 || $2
112
+ from = canonical_path if $1 # `<>` never resolves relative to the includer
113
+ child_canonical, child_content = @resolver.read(request, from: from)
114
+ expand(run, child_canonical, child_content) unless run.pragma_once?(child_canonical)
115
+ needs_marker = true
116
+ else
117
+ if needs_marker
118
+ run.body << "#line #{lineno} #{file_index}" if @line_directives
119
+ # line_directivesの値に関わらず区間は必ず記録する(§8.1の決定事項:
120
+ # テキストの#lineは描画に過ぎず、位置情報の正はこちらが持つ)。
121
+ run.segments << [run.body.size + 1, file_index, lineno]
122
+ needs_marker = false
123
+ end
124
+ run.body << line
125
+ end
126
+ end
127
+
128
+ run.ancestor_stack.pop
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ # ValidatorとManifest.buildが共有する薄い値オブジェクト(§8.3)。
5
+ # sourcesはマージ前の Glslkit::Source をそのまま保持する — E006(同一ステージ内の
6
+ # 重複宣言)はマージ後のデータからは検出できないため、生のReflectionへの
7
+ # アクセスが必要になる。
8
+ class Program
9
+ STAGES = %i[vertex fragment].freeze
10
+
11
+ attr_reader :name, :sources
12
+
13
+ def initialize(name:, sources:)
14
+ unless sources.is_a?(Hash) && !sources.empty? && (sources.keys - STAGES).empty?
15
+ raise ArgumentError, "sources must be a non-empty Hash with only :vertex and/or :fragment keys"
16
+ end
17
+
18
+ @name = name
19
+ @sources = sources
20
+ end
21
+
22
+ def vertex
23
+ sources[:vertex]
24
+ end
25
+
26
+ def fragment
27
+ sources[:fragment]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require_relative "types"
5
+ require_relative "errors"
6
+
7
+ module Glslkit
8
+ # 平坦化済み(#include展開後)のGLSLをスキャンして、トップレベルの
9
+ # in/out/uniform宣言を抽出する。これは意図的に本格的なパーサにはしていない:
10
+ # コメントと、プリプロセッサディレクティブ行(#version, #line, #ifdefなど、
11
+ # glslkit自身のPreprocessorが残しうるもの全般)を除去し、{}のネスト深度を
12
+ # 追跡して深度0の宣言以外(関数本体、ブロック本体)をスキップし、深度0の
13
+ # `;`区切り文を1つずつ正規表現でマッチさせる。struct定義、uniform blockの
14
+ # メンバリスト、const/ローカル宣言は意図的にそれ以上パースしない。
15
+ #
16
+ # 各宣言は`output_line`(渡されたcodeの中での行番号)を保持する(§8.1)。
17
+ # ただしこれはあくまで「渡されたcode文字列の中の行番号」であり、元ファイルの
18
+ # 行番号ではない。元ファイルへの変換はGlslkit::SourceMap#resolveの役目で、
19
+ # ReflectionはSourceMapの存在を知らない(責務を分離するため)。
20
+ class Reflection
21
+ Attribute = Struct.new(:name, :type, :location, :array_size, :output_line, keyword_init: true)
22
+ Uniform = Struct.new(:name, :type, :array_size, :setter, :matrix, :sampler, :output_line, keyword_init: true)
23
+ UniformBlock = Struct.new(:name, :layout, :binding, :output_line, keyword_init: true)
24
+ Output = Struct.new(:name, :type, :location, :output_line, keyword_init: true)
25
+
26
+ IDENT = /[A-Za-z_]\w*/
27
+ PRECISION = /(?:highp|mediump|lowp)\s+/
28
+ ARRAY_SUFFIX = /(?:\s*\[\s*(\d+)\s*\])?/
29
+ LAYOUT_PREFIX = /(?:layout\s*\(([^)]*)\)\s*)?/
30
+
31
+ UNIFORM_BLOCK_PATTERN = /\A#{LAYOUT_PREFIX}uniform\s+(#{IDENT})\s*\{/
32
+ UNIFORM_PATTERN = /\Auniform\s+#{PRECISION}?(#{IDENT})\s+(#{IDENT})#{ARRAY_SUFFIX}\z/
33
+ ATTRIBUTE_PATTERN = /\A#{LAYOUT_PREFIX}in\s+#{PRECISION}?(#{IDENT})\s+(#{IDENT})#{ARRAY_SUFFIX}\z/
34
+ OUTPUT_PATTERN = /\A#{LAYOUT_PREFIX}out\s+#{PRECISION}?(#{IDENT})\s+(#{IDENT})#{ARRAY_SUFFIX}\z/
35
+
36
+ attr_reader :attributes, :uniforms, :uniform_blocks, :outputs
37
+
38
+ def initialize(code)
39
+ @attributes = []
40
+ @uniforms = []
41
+ @uniform_blocks = []
42
+ @outputs = []
43
+
44
+ split_top_level_statements(strip_noise(code)).each do |statement, output_line|
45
+ classify(statement.strip, output_line)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # コメント・ディレクティブ行を除去するが、行数は必ず保つ(§8.1)。
52
+ # split_top_level_statements が数える行番号は、この除去後のテキストに
53
+ # 対して数える(=コメント除去前後で行数が変わらないことが前提)。
54
+ def strip_noise(code)
55
+ without_comments = code.gsub(%r{//[^\n]*|/\*.*?\*/}m) do |match|
56
+ newlines = match.count("\n")
57
+ newlines.positive? ? ("\n" * newlines) : " "
58
+ end
59
+ without_comments.gsub(/^[ \t]*#.*$/, "")
60
+ end
61
+
62
+ # 波括弧の深度が0の時に現れた`;`だけで分割する。これにより関数本体や
63
+ # uniform blockのメンバリスト内の`;`が文の終端として扱われることはない。
64
+ # 深度0に戻る`}`の直後に`;`が続かない場合(つまりuniform blockではなく
65
+ # 関数/制御構文の本体である場合)は、それまで溜めていたものを破棄する。
66
+ #
67
+ # 各要素は [文の本体, 文の(先頭の空白を除いた)開始行番号] のペア。
68
+ def split_top_level_statements(code)
69
+ scanner = StringScanner.new(code)
70
+ statements = []
71
+ buffer = +""
72
+ depth = 0
73
+ line = 1
74
+
75
+ until scanner.eos?
76
+ chunk = scanner.scan_until(/[{};]/)
77
+ break if chunk.nil?
78
+
79
+ buffer << chunk
80
+ line += chunk.count("\n")
81
+
82
+ case chunk[-1]
83
+ when "{"
84
+ depth += 1
85
+ when "}"
86
+ depth -= 1
87
+ buffer = +"" if depth.zero? && !scanner.check(/[ \t\r\n]*;/)
88
+ when ";"
89
+ if depth.zero?
90
+ statements << statement_with_start_line(buffer[0..-2], line)
91
+ buffer = +""
92
+ end
93
+ end
94
+ end
95
+
96
+ statements
97
+ end
98
+
99
+ # rawの末尾(;を除いた部分)がline行目で終わっているとして、rawの
100
+ # 先頭の空白(改行含む)を除いた実内容が何行目から始まるかを逆算する。
101
+ def statement_with_start_line(raw, end_line)
102
+ leading_whitespace = raw[/\A\s*/]
103
+ start_line = end_line - raw.count("\n") + leading_whitespace.count("\n")
104
+ [raw, start_line]
105
+ end
106
+
107
+ def classify(statement, output_line)
108
+ return if statement.empty?
109
+
110
+ case statement
111
+ when UNIFORM_BLOCK_PATTERN
112
+ add_uniform_block($~, output_line)
113
+ when UNIFORM_PATTERN
114
+ add_uniform($~, output_line)
115
+ when ATTRIBUTE_PATTERN
116
+ add_attribute($~, output_line)
117
+ when OUTPUT_PATTERN
118
+ add_output($~, output_line)
119
+ end
120
+ end
121
+
122
+ def add_uniform_block(match, output_line)
123
+ layout_body = match[1]
124
+ @uniform_blocks << UniformBlock.new(
125
+ name: match[2],
126
+ layout: packing_layout(layout_body),
127
+ binding: qualifier_value(layout_body, "binding"),
128
+ output_line: output_line
129
+ )
130
+ end
131
+
132
+ def add_uniform(match, output_line)
133
+ type = match[1]
134
+ @uniforms << Uniform.new(
135
+ name: match[2],
136
+ type: type,
137
+ array_size: (match[3] || 1).to_i,
138
+ setter: Types.setter_for(type),
139
+ matrix: Types.matrix?(type),
140
+ sampler: Types.sampler?(type),
141
+ output_line: output_line
142
+ )
143
+ end
144
+
145
+ def add_attribute(match, output_line)
146
+ @attributes << Attribute.new(
147
+ name: match[3],
148
+ type: match[2],
149
+ location: qualifier_value(match[1], "location"),
150
+ array_size: (match[4] || 1).to_i,
151
+ output_line: output_line
152
+ )
153
+ end
154
+
155
+ def add_output(match, output_line)
156
+ @outputs << Output.new(
157
+ name: match[3],
158
+ type: match[2],
159
+ location: qualifier_value(match[1], "location"),
160
+ output_line: output_line
161
+ )
162
+ end
163
+
164
+ def qualifier_value(layout_body, key)
165
+ return nil unless layout_body
166
+
167
+ m = layout_body.match(/\b#{key}\s*=\s*(\d+)/)
168
+ m && m[1].to_i
169
+ end
170
+
171
+ def packing_layout(layout_body)
172
+ return "shared" unless layout_body
173
+
174
+ m = layout_body.match(/\b(std140|std430|shared|packed)\b/)
175
+ m ? m[1] : "shared"
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ # #include のリクエストを解決するダックタイピングのインタフェース。継承を
5
+ # 強制するものではない — 下の Glslkit::Resolvers::FileSystem と ::Hash は
6
+ # 単にこれと同じメソッド形状を実装しているだけ。Railsや将来のruby.wasm側の
7
+ # resolverも同じ形状に合わせればよい。
8
+ #
9
+ # read(request, from:) -> [canonical_path, content]
10
+ #
11
+ # request - #include の後に書かれた文字列。例 "common/math.glsl"
12
+ # from - 呼び出し元(includeした側)のcanonical_path。
13
+ # エントリポイントではnil (`#include <...>` の場合も
14
+ # 常にnilを渡し、相対探索を完全にスキップさせる)
15
+ # canonical_path - 解決したファイルを表す、load_path相対の安定した
16
+ # 識別子 (循環検出、#pragma once、
17
+ # Glslkit::SourceMap#files で使う)
18
+ #
19
+ # requestが解決できない場合は Glslkit::IncludeNotFound を raise すること。
20
+ module Resolver
21
+ end
22
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module Glslkit
6
+ module Resolvers
7
+ # ディスク上のload_paths群に対して#includeのリクエストを解決する。
8
+ # canonical_pathは常に、マッチしたload_pathからの相対パスとして表現される
9
+ # (例 "common/math.glsl")。絶対パスになることはない —
10
+ # この結果を元に作られるマニフェストはブラウザに配信されるものなので、
11
+ # ローカルの絶対パスを漏らしてはならない。
12
+ class FileSystem
13
+ def initialize(load_paths:)
14
+ @load_paths = load_paths.map { |path| File.expand_path(path) }
15
+ @absolute_paths = {} # canonical_path => 絶対パス。相対探索のためのキャッシュ
16
+ end
17
+
18
+ def read(request, from: nil)
19
+ absolute = resolve_absolute_path(request, from)
20
+ canonical = canonicalize(absolute)
21
+ @absolute_paths[canonical] = absolute
22
+ [canonical, File.read(absolute)]
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_absolute_path(request, from)
28
+ candidates = []
29
+ candidates << File.expand_path(File.join(File.dirname(@absolute_paths.fetch(from)), request)) if from
30
+ @load_paths.each { |load_path| candidates << File.expand_path(File.join(load_path, request)) }
31
+
32
+ candidates.each do |candidate|
33
+ next unless within_any_load_path?(candidate)
34
+ return candidate if File.file?(candidate)
35
+ end
36
+
37
+ # 存在確認より先に境界チェックを行う(escapeしているのにたまたま存在
38
+ # しないrequestに対してIncludeNotFoundを出すのではなく)。これにより
39
+ # PathTraversalErrorの判定が、load_paths外に実際に何が存在するかに
40
+ # 依存しなくなる。
41
+ if candidates.any? { |candidate| !within_any_load_path?(candidate) }
42
+ raise PathTraversalError, "#include #{request.inspect} escapes the configured load_paths"
43
+ end
44
+
45
+ raise IncludeNotFound, "could not resolve #include #{request.inspect}"
46
+ end
47
+
48
+ def within_any_load_path?(candidate)
49
+ @load_paths.any? { |load_path| within?(load_path, candidate) }
50
+ end
51
+
52
+ def within?(load_path, candidate)
53
+ candidate == load_path || candidate.start_with?("#{load_path}#{File::SEPARATOR}")
54
+ end
55
+
56
+ def canonicalize(absolute)
57
+ load_path = @load_paths.find { |lp| within?(lp, absolute) }
58
+ absolute.delete_prefix("#{load_path}#{File::SEPARATOR}")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module Glslkit
6
+ module Resolvers
7
+ # {request => content} というフラットなHashを直接引くだけ。ファイル
8
+ # システムを持たないwasm側ランタイムやテストで使う。`from`はインタ
9
+ # フェースの形を合わせるために受け取るが無視する。ディレクトリ構造が
10
+ # 存在しないため、相対解決のしようがない。
11
+ class Hash
12
+ def initialize(files)
13
+ @files = files
14
+ end
15
+
16
+ def read(request, from: nil)
17
+ content = @files[request]
18
+ raise IncludeNotFound, "no such include: #{request.inspect}" unless content
19
+
20
+ [request, content]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # マニフェストの読み込みと SourceMap の復元だけに必要な、狭い入口(M8g)。
4
+ # ruby.wasm 上の消費側(glslkit-webgl 等)からはこちらを require すること。
5
+ #
6
+ # `glslkit/preprocessor` / `glslkit/reflection` / `glslkit/minifier` /
7
+ # `glslkit/digest` は意図的にロードしない。ビルド用途(前処理・解析・
8
+ # digest計算)は従来通り `require "glslkit"`(core/lib/glslkit.rb)を使うこと。
9
+ #
10
+ # `glslkit/diagnostic` は例外(M11d、Context#reload_programの戻り値が使う)。
11
+ # stdlib依存も他ファイルへのrequireも無いプレーンなStructなので、狭い入口の
12
+ # 制約(File/Dir/digestを持ち込まない)を破らない。
13
+ require_relative "errors"
14
+ require_relative "types"
15
+ require_relative "source_map"
16
+ require_relative "manifest"
17
+ require_relative "diagnostic"
18
+
19
+ module Glslkit
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ Source = Struct.new(:code, :source_map, :reflection, :digest, keyword_init: true)
5
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ # 出力される `#line <n> <index>` ディレクティブで使われるファイル
5
+ # インデックスを、各インデックスが指すcanonical_pathに逆引きする。
6
+ #
7
+ # あわせて「平坦化後の出力行 → (元ファイル, 元の行)」の区間マッピングを
8
+ # 保持する(§8.1)。テキスト上の`#line`ディレクティブはこのマッピングを
9
+ # 人間/GLSLコンパイラ向けに描画したものに過ぎず、`line_directives: false`
10
+ # で抑止されていてもこちらは常に記録される。
11
+ class SourceMap
12
+ class UnsupportedVersionError < StandardError; end
13
+
14
+ Segment = Struct.new(:output_line, :file_index, :source_line)
15
+ private_constant :Segment
16
+
17
+ # spec/schema/source-map-v1.json に対応するHashから復元する(M8g)。
18
+ # `segments` は `output_line` 昇順であることを前提にする(`to_h` は
19
+ # 常にソート済みで出力する)。ソートされていない入力を渡した場合の
20
+ # `resolve` の結果は未定義— 呼び出し側で保証すること。
21
+ def self.from_h(hash)
22
+ version = hash.fetch("version")
23
+ raise UnsupportedVersionError, "unsupported source map version: #{version.inspect}" unless version == 1
24
+
25
+ source_map = new
26
+ hash.fetch("files").each { |path| source_map.index_for(path) }
27
+ hash.fetch("segments").each do |output_line, file_index, source_line|
28
+ source_map.add_segment(output_line: output_line, file_index: file_index, source_line: source_line)
29
+ end
30
+ source_map
31
+ end
32
+
33
+ def initialize
34
+ @files = []
35
+ @index_by_path = {}
36
+ @segments = []
37
+ end
38
+
39
+ attr_reader :files
40
+
41
+ # spec/schema/source-map-v1.json に対応するHashを返す。`segments` は
42
+ # 呼び出し順(=Preprocessorがoutput_line昇順で呼ぶ順)を信頼せず、
43
+ # 明示的に `output_line` 昇順にソートしてから出力する。
44
+ def to_h
45
+ {
46
+ "version" => 1,
47
+ "files" => @files,
48
+ "segments" => @segments.sort_by(&:output_line).map { |s| [s.output_line, s.file_index, s.source_line] }
49
+ }
50
+ end
51
+
52
+ def index_for(path)
53
+ @index_by_path[path] ||= begin
54
+ @files << path
55
+ @files.size - 1
56
+ end
57
+ end
58
+
59
+ # output_line以降、次のadd_segment呼び出しまで(または出力の終わりまで)は
60
+ # file_indexのファイルのsource_line以降に1対1で対応する、という区間を
61
+ # 登録する。呼び出し順はoutput_line昇順であること(Preprocessorはこの順で
62
+ # 呼ぶ)。
63
+ def add_segment(output_line:, file_index:, source_line:)
64
+ @segments << Segment.new(output_line, file_index, source_line)
65
+ end
66
+
67
+ # output_lineが属する区間を二分探索し、[canonical_path, source_line]を
68
+ # 返す。どの区間にも属さない(記録前の行、または区間が1つも無い)場合はnil。
69
+ def resolve(output_line)
70
+ segment = segment_for(output_line)
71
+ return nil unless segment
72
+
73
+ [@files[segment.file_index], segment.source_line + (output_line - segment.output_line)]
74
+ end
75
+
76
+ private
77
+
78
+ def segment_for(output_line)
79
+ return nil if @segments.empty?
80
+
81
+ index = @segments.bsearch_index { |segment| segment.output_line > output_line }
82
+ index = @segments.size if index.nil?
83
+ return nil if index.zero?
84
+
85
+ @segments[index - 1]
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Glslkit
6
+ # GLSLの型名をWebGL2のsetterに対応付ける唯一の正。wasm側のWebGL
7
+ # バインディングはこのテーブルを自前で再実装するのではなく、
8
+ # Types.setter_for("mat4") 等を呼び出すことを想定している。
9
+ module Types
10
+ NON_MATRIX_SETTERS = {
11
+ "float" => "uniform1fv",
12
+ "vec2" => "uniform2fv",
13
+ "vec3" => "uniform3fv",
14
+ "vec4" => "uniform4fv",
15
+ "int" => "uniform1iv",
16
+ "bool" => "uniform1iv",
17
+ "ivec2" => "uniform2iv",
18
+ "bvec2" => "uniform2iv",
19
+ "ivec3" => "uniform3iv",
20
+ "bvec3" => "uniform3iv",
21
+ "ivec4" => "uniform4iv",
22
+ "bvec4" => "uniform4iv",
23
+ "uint" => "uniform1uiv",
24
+ "uvec2" => "uniform2uiv",
25
+ "uvec3" => "uniform3uiv",
26
+ "uvec4" => "uniform4uiv"
27
+ }.freeze
28
+
29
+ MATRIX_SETTERS = {
30
+ "mat2" => "uniformMatrix2fv",
31
+ "mat3" => "uniformMatrix3fv",
32
+ "mat4" => "uniformMatrix4fv",
33
+ "mat2x3" => "uniformMatrix2x3fv",
34
+ "mat2x4" => "uniformMatrix2x4fv",
35
+ "mat3x2" => "uniformMatrix3x2fv",
36
+ "mat3x4" => "uniformMatrix3x4fv",
37
+ "mat4x2" => "uniformMatrix4x2fv",
38
+ "mat4x3" => "uniformMatrix4x3fv"
39
+ }.freeze
40
+
41
+ SAMPLER_TYPES = %w[
42
+ sampler2D sampler3D samplerCube sampler2DArray
43
+ isampler2D isampler3D isamplerCube isampler2DArray
44
+ usampler2D usampler3D usamplerCube usampler2DArray
45
+ sampler2DShadow samplerCubeShadow sampler2DArrayShadow
46
+ ].freeze
47
+
48
+ # 1要素あたりの成分数(§8.5)。setterに渡す配列の期待長(= components * array_size)の
49
+ # 算出に使う。
50
+ NON_MATRIX_COMPONENTS = {
51
+ "float" => 1, "int" => 1, "bool" => 1, "uint" => 1,
52
+ "vec2" => 2, "ivec2" => 2, "bvec2" => 2, "uvec2" => 2,
53
+ "vec3" => 3, "ivec3" => 3, "bvec3" => 3, "uvec3" => 3,
54
+ "vec4" => 4, "ivec4" => 4, "bvec4" => 4, "uvec4" => 4
55
+ }.freeze
56
+
57
+ # matCxR は C列R行の行列(GLSL仕様)。成分数は C * R。
58
+ MATRIX_COMPONENTS = {
59
+ "mat2" => 4, "mat3" => 9, "mat4" => 16,
60
+ "mat2x3" => 6, "mat2x4" => 8,
61
+ "mat3x2" => 6, "mat3x4" => 12,
62
+ "mat4x2" => 8, "mat4x3" => 12
63
+ }.freeze
64
+
65
+ ENTRIES = {
66
+ **NON_MATRIX_SETTERS.to_h { |type, setter|
67
+ [type, {setter: setter, matrix: false, sampler: false, components: NON_MATRIX_COMPONENTS.fetch(type)}]
68
+ },
69
+ **MATRIX_SETTERS.to_h { |type, setter|
70
+ [type, {setter: setter, matrix: true, sampler: false, components: MATRIX_COMPONENTS.fetch(type)}]
71
+ },
72
+ **SAMPLER_TYPES.to_h { |type| [type, {setter: "uniform1iv", matrix: false, sampler: true, components: 1}] }
73
+ }.freeze
74
+
75
+ module_function
76
+
77
+ def setter_for(type)
78
+ entry_for(type)[:setter]
79
+ end
80
+
81
+ def matrix?(type)
82
+ entry_for(type)[:matrix]
83
+ end
84
+
85
+ def sampler?(type)
86
+ entry_for(type)[:sampler]
87
+ end
88
+
89
+ def components_for(type)
90
+ entry_for(type)[:components]
91
+ end
92
+
93
+ def entry_for(type)
94
+ ENTRIES.fetch(type) { raise UnknownTypeError, "unknown GLSL type: #{type.inspect}" }
95
+ end
96
+ end
97
+ end