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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/glslkit.gemspec +25 -0
- data/lib/glslkit/bundle.rb +118 -0
- data/lib/glslkit/diagnostic.rb +27 -0
- data/lib/glslkit/digest.rb +18 -0
- data/lib/glslkit/errors.rb +14 -0
- data/lib/glslkit/manifest.rb +198 -0
- data/lib/glslkit/minifier.rb +33 -0
- data/lib/glslkit/preprocessor.rb +131 -0
- data/lib/glslkit/program.rb +30 -0
- data/lib/glslkit/reflection.rb +178 -0
- data/lib/glslkit/resolver.rb +22 -0
- data/lib/glslkit/resolvers/file_system.rb +62 -0
- data/lib/glslkit/resolvers/hash.rb +24 -0
- data/lib/glslkit/runtime.rb +20 -0
- data/lib/glslkit/source.rb +5 -0
- data/lib/glslkit/source_map.rb +88 -0
- data/lib/glslkit/types.rb +97 -0
- data/lib/glslkit/validator.rb +228 -0
- data/lib/glslkit/version.rb +5 -0
- data/lib/glslkit.rb +21 -0
- metadata +67 -0
|
@@ -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,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
|