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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 13d9773aaab7e412f3a84693c9c5906310180722565fb870b64000b9bf8ea74a
|
|
4
|
+
data.tar.gz: 6dc9b85466232122da5d012bfe251564eee853c33b369c1cf2e73590bed9fd64
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fea6181044d5c985678d050bdeb582b002c63d435b54c569e489b0fbf7aeef4c36bfbbff72ef6742bb64974e56b2fc5a3fe1c6d493f8f7ec2b855e70e40b8552
|
|
7
|
+
data.tar.gz: 6a6d411e5e9f541f65742aac540c279e68b637b846a9c43f4f14cedd6a4d6c415e04a7a19332f7ba1cba93c8a06bfb1d93039fdb57a6ba8eaff54f30d22f1929
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
初回リリース (v0.1)。
|
|
6
|
+
|
|
7
|
+
### 追加
|
|
8
|
+
|
|
9
|
+
- `Glslkit::Preprocessor` — `#include`解決、`#pragma once`、`#version`/`#extension`の
|
|
10
|
+
集約と衝突検出、`#line`ディレクティブ挿入、循環include検出
|
|
11
|
+
- `Glslkit::Resolvers::FileSystem` / `Glslkit::Resolvers::Hash`
|
|
12
|
+
- `Glslkit::Reflection` — attribute / uniform / uniform_block / output の抽出
|
|
13
|
+
- `Glslkit::Types` — GLSL型 → WebGL2 setter の対応表
|
|
14
|
+
- `Glslkit::Manifest` — `spec/schema/reflection-v1.json` (Draft 2020-12) に
|
|
15
|
+
準拠したマニフェスト生成。ステージ間のuniform/uniform_block統合
|
|
16
|
+
- `Glslkit::Minifier` — コメント・空白のみの除去 (識別子リネームなし)
|
|
17
|
+
- `Glslkit::SourceMap` / `Glslkit::Source`
|
|
18
|
+
- `Glslkit::Validator` — E001〜E007 / W001〜W004 の検証ルール(ステージ間の
|
|
19
|
+
out/in不一致、cross-stage uniform型不一致、attribute/output location衝突、
|
|
20
|
+
ステージ内の重複宣言、`gl_`予約プレフィックス)。例外を投げず診断の配列を返す
|
|
21
|
+
- `Glslkit::Diagnostic` / `Glslkit::Program` — Validator/Manifest.build/
|
|
22
|
+
ライブリロードが共有する値オブジェクト
|
|
23
|
+
- `Glslkit::Bundle` — 前処理→Program化→検証→Manifest組み立てをまとめた
|
|
24
|
+
中間表現。core/rails/wasmのどれにも属さず、`glslkit-webgl`の埋め込み生成
|
|
25
|
+
(`rake glslkit:embed`)と`glslkit-rails`のライブリロードエンドポイントの
|
|
26
|
+
両方から使われる。ポーリング用のソースdigestも計算する
|
|
27
|
+
- `Glslkit::Digest` — stdlib `digest` への依存をこのファイルだけに閉じ込める
|
|
28
|
+
- `require "glslkit/runtime"` — マニフェストの読み込みとSourceMapの復元だけに
|
|
29
|
+
必要な狭い入口。`glslkit-webgl`(ruby.wasm)側はこちらを使い、
|
|
30
|
+
前処理・解析・digest計算のコードを読み込まない
|
|
31
|
+
- `Glslkit::Manifest` の `generator` フィールド — マニフェストが
|
|
32
|
+
`rake glslkit:embed`/`glslkit-rails`のどちらで生成されたかを来歴として残す
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kyubey1228
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# glslkit
|
|
2
|
+
|
|
3
|
+
GLSLソースを前処理・解析し、ビルド時に得たリフレクション情報(uniform / attribute
|
|
4
|
+
/ output / uniform block)をJSONとして残すためのRuby gem。実行時依存はゼロ
|
|
5
|
+
(stdlibの `json` `strscan` `set` `digest` のみ)で、将来 [ruby.wasm](https://github.com/ruby/ruby.wasm)
|
|
6
|
+
上のWebGLバインディングにそのまま組み込めることを狙っている。
|
|
7
|
+
|
|
8
|
+
Rails (Propshaft) との統合は別gem [`glslkit-rails`](../rails) が提供する。
|
|
9
|
+
|
|
10
|
+
## インストール
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "glslkit"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 使い方
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
require "glslkit"
|
|
20
|
+
|
|
21
|
+
resolver = Glslkit::Resolvers::FileSystem.new(load_paths: ["app/shaders"])
|
|
22
|
+
source = Glslkit::Preprocessor.new(resolver: resolver).process("pbr.frag")
|
|
23
|
+
|
|
24
|
+
source.code # => 平坦化済みGLSL (String)。#include解決済み、#lineディレクティブ付き
|
|
25
|
+
source.reflection # => Glslkit::Reflection (attributes / uniforms / uniform_blocks / outputs)
|
|
26
|
+
source.source_map # => Glslkit::SourceMap (#lineのファイルインデックス → パス)
|
|
27
|
+
source.digest # => SHA256 hex (String)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `#include` の解決
|
|
31
|
+
|
|
32
|
+
```glsl
|
|
33
|
+
#include "common/math.glsl" // 相対探索 → load_pathsの順に探索
|
|
34
|
+
#include <glslkit/pbr.glsl> // load_pathsのみを探索(相対探索はしない)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- 循環includeは `Glslkit::CircularIncludeError`
|
|
38
|
+
- `#pragma once` は2回目以降の展開をスキップ(デフォルトは毎回展開)
|
|
39
|
+
- `#version` は複数ファイルに散在していても1行目に1回だけ出力し、値が食い違えば
|
|
40
|
+
`Glslkit::VersionConflictError`
|
|
41
|
+
- `#if` / `#ifdef` / `#else` / `#endif` は評価せずそのまま出力する(内部の
|
|
42
|
+
`#include` は展開される。これはv0.1の既知の制限)
|
|
43
|
+
|
|
44
|
+
wasm側でファイルシステムの無い環境向けに `Glslkit::Resolvers::Hash` も同梱している:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
resolver = Glslkit::Resolvers::Hash.new(
|
|
48
|
+
"pbr.frag" => "...",
|
|
49
|
+
"common/math.glsl" => "..."
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### リフレクション抽出
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
source.reflection.attributes # => [#<Attribute name="a_position" type="vec3" location=0 array_size=1>, ...]
|
|
57
|
+
source.reflection.uniforms # => [#<Uniform name="u_mvp" type="mat4" setter="uniformMatrix4fv" matrix=true sampler=false ...>, ...]
|
|
58
|
+
source.reflection.uniform_blocks # => [#<UniformBlock name="Camera" layout="std140" binding=0>, ...]
|
|
59
|
+
source.reflection.outputs # => [#<Output name="fragColor" type="vec4" location=0>, ...]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
型からWebGL2のsetterへの対応表は `Glslkit::Types` が唯一の正:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Glslkit::Types.setter_for("mat4") # => "uniformMatrix4fv"
|
|
66
|
+
Glslkit::Types.matrix?("mat4") # => true
|
|
67
|
+
Glslkit::Types.sampler?("sampler2D") # => true
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### マニフェスト生成
|
|
71
|
+
|
|
72
|
+
vertex/fragmentの `Source` ペアから、`spec/schema/reflection-v1.json`
|
|
73
|
+
(Draft 2020-12 JSON Schema) に準拠したマニフェストを組み立てる:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
manifest = Glslkit::Manifest.new(generated_at: Time.now.utc.iso8601)
|
|
77
|
+
manifest.add_program("pbr",
|
|
78
|
+
vertex: {path: "pbr.vert", source: vertex_source, url: "/assets/pbr-a1b2.vert"},
|
|
79
|
+
fragment: {path: "pbr.frag", source: fragment_source, url: "/assets/pbr-c3d4.frag"})
|
|
80
|
+
|
|
81
|
+
manifest.to_json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
同名uniform/uniform_blockはステージ間で1エントリに統合され、型
|
|
85
|
+
(またはlayout/binding)が食い違えば `Glslkit::StageMismatchError` になる。
|
|
86
|
+
|
|
87
|
+
### Minify
|
|
88
|
+
|
|
89
|
+
コメント・空白の除去のみを行う(識別子リネームはしない)。改行は一切削除しない
|
|
90
|
+
ため、`#line` ディレクティブの行番号や `Reflection` の抽出結果を壊さない:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
Glslkit::Minifier.minify(source.code)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## エラークラス
|
|
97
|
+
|
|
98
|
+
`Glslkit::IncludeNotFound`, `Glslkit::PathTraversalError`,
|
|
99
|
+
`Glslkit::CircularIncludeError`, `Glslkit::VersionConflictError`,
|
|
100
|
+
`Glslkit::StageMismatchError`, `Glslkit::UnknownTypeError`
|
|
101
|
+
|
|
102
|
+
## ライセンス
|
|
103
|
+
|
|
104
|
+
[MIT](LICENSE.txt)
|
data/glslkit.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/glslkit/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "glslkit"
|
|
7
|
+
spec.version = Glslkit::VERSION
|
|
8
|
+
spec.authors = ["kyubey1228"]
|
|
9
|
+
spec.email = ["kyuuka1228@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "GLSL preprocessing and reflection toolkit"
|
|
12
|
+
spec.description = "Flattens #include directives in GLSL shaders and extracts " \
|
|
13
|
+
"uniform/attribute/output reflection data as JSON, with zero runtime " \
|
|
14
|
+
"dependencies so it can run under ruby.wasm."
|
|
15
|
+
spec.homepage = "https://github.com/kyubey1228/glslkit"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.1"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main/core"
|
|
21
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/core/CHANGELOG.md"
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.glob("lib/**/*.rb") + ["glslkit.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"]
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "preprocessor"
|
|
5
|
+
require_relative "program"
|
|
6
|
+
require_relative "validator"
|
|
7
|
+
require_relative "manifest"
|
|
8
|
+
require_relative "digest"
|
|
9
|
+
|
|
10
|
+
module Glslkit
|
|
11
|
+
# 1プログラム分(vertex+fragment)を「前処理→Program化→検証→Manifest組み立て」
|
|
12
|
+
# まで通す中間表現(M11b、SPEC-livereload.md §3.2)。core/rails/wasmのどれにも
|
|
13
|
+
# 属さない — `Glslkit::WebGL::Embed` はこれを `.rb` に整形するだけの薄い
|
|
14
|
+
# ラッパーになり、Rails側のライブリロードは同じ結果をJSONに整形して返す
|
|
15
|
+
# (シリアライズ形式だけが違う、というのがこのクラスを切り出した理由)。
|
|
16
|
+
#
|
|
17
|
+
# `.build` は例外を投げない。前処理(#include解決)が失敗した場合も、
|
|
18
|
+
# Validatorがエラーを返した場合も、結果は常に Result として返る。
|
|
19
|
+
# 例外にして呼び出し側に伝える/伝えないの判断は呼び出し側の役目
|
|
20
|
+
# (Embedは前者、Railsのライブリロードエンドポイントは後者になる想定)。
|
|
21
|
+
module Bundle
|
|
22
|
+
# Preprocessor#process とその内部で使うResolverが実際に投げうる例外を
|
|
23
|
+
# 尽くす。ここに列挙されていない例外(想定外のバグ等)は握り潰さず
|
|
24
|
+
# そのまま伝播させる。
|
|
25
|
+
PREPROCESS_ERRORS = [
|
|
26
|
+
IncludeNotFound,
|
|
27
|
+
CircularIncludeError,
|
|
28
|
+
VersionConflictError,
|
|
29
|
+
PathTraversalError
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# kind: nil(成功) | :preprocess(#include解決の失敗) | :validation(Validatorのエラー)。
|
|
33
|
+
# kind == :validation でも vertex/fragment は入る(前処理自体は成功している)。
|
|
34
|
+
# manifest は kind.nil? のときだけ入る。
|
|
35
|
+
class Result
|
|
36
|
+
attr_reader :name, :kind, :source_digest, :vertex, :fragment, :manifest, :diagnostics, :error
|
|
37
|
+
|
|
38
|
+
def initialize(name:, source_digest:, kind: nil, vertex: nil, fragment: nil, manifest: nil, diagnostics: [], error: nil)
|
|
39
|
+
@name = name
|
|
40
|
+
@kind = kind
|
|
41
|
+
@source_digest = source_digest
|
|
42
|
+
@vertex = vertex
|
|
43
|
+
@fragment = fragment
|
|
44
|
+
@manifest = manifest
|
|
45
|
+
@diagnostics = diagnostics
|
|
46
|
+
@error = error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ok?
|
|
50
|
+
kind.nil?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
# resolver: #include解決に使うResolver(呼び出し側が用意する。FileSystem/Hash等)。
|
|
57
|
+
# vertex/fragment: resolverへ渡すエントリポイントのリクエスト文字列(相対パス)。
|
|
58
|
+
# urls: Manifestのstage.urlに載せる値。省略時はvertex/fragmentのパス自身を使う
|
|
59
|
+
# (Embedの用途。実URLが要るRails側は明示的に渡す)。
|
|
60
|
+
# known_files: ポーリング用digest(§3.2 決定1)を、前処理が失敗した回にも
|
|
61
|
+
# 計算するための「前回成功時のfiles」。呼び出し側が持ち回る
|
|
62
|
+
# (Bundle自身はステートレスに保つ)。
|
|
63
|
+
def build(resolver:, name:, vertex:, fragment:, line_directives: true, disabled_checks: [],
|
|
64
|
+
urls: {vertex: vertex, fragment: fragment}, known_files: [], now: Time.now)
|
|
65
|
+
preprocessor = Preprocessor.new(resolver: resolver, line_directives: line_directives)
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
vertex_source = preprocessor.process(vertex)
|
|
69
|
+
fragment_source = preprocessor.process(fragment)
|
|
70
|
+
rescue *PREPROCESS_ERRORS => e
|
|
71
|
+
digest = source_digest(resolver, fallback_files(known_files, [vertex, fragment]))
|
|
72
|
+
return Result.new(name: name, kind: :preprocess, source_digest: digest, error: e)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
program = Program.new(name: name, sources: {vertex: vertex_source, fragment: fragment_source})
|
|
76
|
+
validation = Validator.new(disabled: disabled_checks).validate(program)
|
|
77
|
+
digest = source_digest(resolver, (vertex_source.source_map.files + fragment_source.source_map.files).uniq)
|
|
78
|
+
|
|
79
|
+
unless validation.ok?
|
|
80
|
+
return Result.new(
|
|
81
|
+
name: name, kind: :validation, source_digest: digest,
|
|
82
|
+
vertex: vertex_source, fragment: fragment_source, diagnostics: validation.diagnostics
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
manifest = Manifest.build(programs: [program], urls: {name => urls}, now: now)
|
|
87
|
+
Result.new(
|
|
88
|
+
name: name, source_digest: digest, vertex: vertex_source, fragment: fragment_source,
|
|
89
|
+
manifest: manifest, diagnostics: validation.diagnostics
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def fallback_files(known_files, entry_points)
|
|
94
|
+
(known_files.empty? ? entry_points : known_files + entry_points).uniq
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 依存する全ソースファイルの生バイト列(前処理後のコードではない)を
|
|
98
|
+
# パスでソートして "path\0content" で連結し、SHA256を取る(決定1)。
|
|
99
|
+
def source_digest(resolver, files)
|
|
100
|
+
entries = files.sort.filter_map do |path|
|
|
101
|
+
content = read_raw(resolver, path)
|
|
102
|
+
"#{path}\0#{content}" unless content.nil?
|
|
103
|
+
end
|
|
104
|
+
Digest.hexdigest(entries.join)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# digest計算専用の再読み込み。known_filesに含まれる「前回成功時のファイル」が
|
|
108
|
+
# その後削除されている等で読めなくなっていても、digest計算そのものは
|
|
109
|
+
# 落とさない(ポーリングを壊さないことを優先する。digestは変化を検知できれば
|
|
110
|
+
# 十分で、読めなかった1ファイルの厳密な反映までは求めない)。
|
|
111
|
+
def read_raw(resolver, path)
|
|
112
|
+
_, content = resolver.read(path, from: nil)
|
|
113
|
+
content
|
|
114
|
+
rescue
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
# 検証結果1件分(§8.2)。file/lineの解決はValidatorの役目であり、
|
|
5
|
+
# DiagnosticやReflection自身は関与しない。
|
|
6
|
+
Diagnostic = Struct.new(:severity, :code, :message, :program, :stage, :name, :file, :line, keyword_init: true) do
|
|
7
|
+
def error?
|
|
8
|
+
severity == :error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def warning?
|
|
12
|
+
severity == :warning
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
"[#{code}] #{location}: #{message}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def location
|
|
22
|
+
return "#{file}:#{line}" if file
|
|
23
|
+
|
|
24
|
+
stage ? "#{program}(#{stage})" : program.to_s
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Glslkit
|
|
6
|
+
# stdlibの `digest` を要求するのはこのファイルだけにする(M8g)。
|
|
7
|
+
# `glslkit/runtime`(wasm向けの狭い入口)はこのファイルをrequireしないため、
|
|
8
|
+
# マニフェストの読み込み・SourceMapの復元だけならdigestはロードされない。
|
|
9
|
+
module Digest
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# 定数探索はこのモジュール自身より先にこのレキシカルスコープを見るため、
|
|
13
|
+
# 曖昧さを避けて常にトップレベルの `::Digest` を明示する。
|
|
14
|
+
def hexdigest(string)
|
|
15
|
+
::Digest::SHA256.hexdigest(string)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
class UnknownTypeError < StandardError; end
|
|
5
|
+
class IncludeNotFound < StandardError; end
|
|
6
|
+
class PathTraversalError < StandardError; end
|
|
7
|
+
class CircularIncludeError < StandardError; end
|
|
8
|
+
class VersionConflictError < StandardError; end
|
|
9
|
+
class StageMismatchError < StandardError; end
|
|
10
|
+
# M11a: glslkit-railsのassets:precompileフックが、Validatorのエラーで
|
|
11
|
+
# ビルドを止めるときに投げる(SPEC.md §8.6)。coreはこれを自分では投げない
|
|
12
|
+
# (Rails側からしか使われないが、例外クラス自体は名前空間の都合でcoreに置く)。
|
|
13
|
+
class ValidationFailed < StandardError; end
|
|
14
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "types"
|
|
6
|
+
require_relative "version"
|
|
7
|
+
|
|
8
|
+
module Glslkit
|
|
9
|
+
# プログラムごとのvertex+fragment Sourceペアから reflection-v1.json の
|
|
10
|
+
# マニフェスト(spec/schema/reflection-v1.json)を組み立てる。各stageの
|
|
11
|
+
# `url`は呼び出し側から渡される不透明な文字列(例: Propshaftの
|
|
12
|
+
# フィンガープリント付きアセットパス)である — glslkit自身のdigestとは
|
|
13
|
+
# 無関係で、digestから導出することはなく、渡された値をそのまま格納する
|
|
14
|
+
# だけである。
|
|
15
|
+
class Manifest
|
|
16
|
+
class UnsupportedVersionError < StandardError; end
|
|
17
|
+
|
|
18
|
+
# JSON文字列(またはJSON.parse済みのHash)からManifestを復元する(M8g)。
|
|
19
|
+
# `glslkit/runtime`(digestをロードしない狭い入口)から使われる想定のため、
|
|
20
|
+
# ここではdigestに一切触れない。検証は行わない — 壊れた入力は
|
|
21
|
+
# KeyError/JSON::ParserError等をそのまま投げる。
|
|
22
|
+
def self.parse(json_string_or_hash)
|
|
23
|
+
hash = json_string_or_hash.is_a?(String) ? JSON.parse(json_string_or_hash) : json_string_or_hash
|
|
24
|
+
version = hash.fetch("schema_version")
|
|
25
|
+
raise UnsupportedVersionError, "unsupported schema_version: #{version.inspect}" unless version == 1
|
|
26
|
+
|
|
27
|
+
new(generated_at: hash.fetch("generated_at"), programs: hash.fetch("programs"), generator: hash["generator"])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Glslkit::Program (§8.3) の配列からマニフェストのHashを直接組み立てる。
|
|
31
|
+
# Validatorと入力を共有できるようにする入口で、内部的には
|
|
32
|
+
# add_program に委譲するだけ(既存のマージロジックには手を入れない)。
|
|
33
|
+
#
|
|
34
|
+
# vertex/fragmentが両方揃っていないProgramは(共有partial等の可能性が
|
|
35
|
+
# あるため)黙ってスキップする — SPEC.md §2.4/§3の「プログラムは
|
|
36
|
+
# 両ステージ必須」という決定と、ManifestBuilder(rails側)が既に
|
|
37
|
+
# 行っているのと同じ扱い。
|
|
38
|
+
#
|
|
39
|
+
# 検証(Validator)は必ずこれより先に呼ぶこと(§8.6)。ここは今まで通り
|
|
40
|
+
# StageMismatchErrorを投げる経路のままなので、検証を経ていない入力を
|
|
41
|
+
# 渡すと診断を得る前に例外で落ちる。
|
|
42
|
+
def self.build(programs:, urls: {}, now: Time.now)
|
|
43
|
+
# digest(§8.9)と同じ扱い: `.iso8601` はstdlibの"time"の拡張なので、
|
|
44
|
+
# 実際に使う`.build`経路でだけ遅延require する。`glslkit/runtime`
|
|
45
|
+
# (`.parse`/`.to_h`のみ)はこのメソッドを経由しないため影響しない。
|
|
46
|
+
require "time"
|
|
47
|
+
manifest = new(generated_at: now.utc.iso8601, generator: "glslkit/#{Glslkit::VERSION}")
|
|
48
|
+
programs.each do |program|
|
|
49
|
+
next unless program.sources.key?(:vertex) && program.sources.key?(:fragment)
|
|
50
|
+
|
|
51
|
+
manifest.add_program(
|
|
52
|
+
program.name,
|
|
53
|
+
vertex: stage_input(program, :vertex, urls),
|
|
54
|
+
fragment: stage_input(program, :fragment, urls)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
manifest.to_h
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.stage_input(program, stage, urls)
|
|
61
|
+
ext = (stage == :vertex) ? "vert" : "frag"
|
|
62
|
+
{path: "#{program.name}.#{ext}", source: program.sources.fetch(stage), url: urls.dig(program.name, stage)}
|
|
63
|
+
end
|
|
64
|
+
private_class_method :stage_input
|
|
65
|
+
|
|
66
|
+
def initialize(generated_at:, programs: {}, generator: nil)
|
|
67
|
+
@generated_at = generated_at
|
|
68
|
+
@programs = programs
|
|
69
|
+
@generator = generator
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_program(name, vertex:, fragment:)
|
|
73
|
+
@programs[name.to_s] = build_program(vertex: vertex, fragment: fragment)
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# `generator`(M10e)は「このManifestを実際に組み立てたのは何か」を表す
|
|
78
|
+
# 来歴情報。`build`が呼ばれたときにだけそのバージョンで確定させ、`to_h`が
|
|
79
|
+
# 呼ばれるたびに現在実行中のバージョンで上書きしたりはしない —
|
|
80
|
+
# `parse`で読み込んだ古いバージョンの生成物の来歴を破壊しないため。
|
|
81
|
+
# 未設定(手書きのManifestや、generatorを持たない古い生成物)なら
|
|
82
|
+
# キー自体を出さない。`reflection-v1.json`でoptional(型はstring)なので
|
|
83
|
+
# nullを出すとスキーマ違反になる。
|
|
84
|
+
def to_h
|
|
85
|
+
{
|
|
86
|
+
"schema_version" => 1,
|
|
87
|
+
"generated_at" => @generated_at,
|
|
88
|
+
"generator" => @generator,
|
|
89
|
+
"programs" => @programs
|
|
90
|
+
}.compact
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_json(*args)
|
|
94
|
+
JSON.generate(to_h, *args)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def build_program(vertex:, fragment:)
|
|
100
|
+
vertex_reflection = vertex.fetch(:source).reflection
|
|
101
|
+
fragment_reflection = fragment.fetch(:source).reflection
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
"digest" => program_digest(vertex.fetch(:source).digest, fragment.fetch(:source).digest),
|
|
105
|
+
"stages" => {
|
|
106
|
+
"vertex" => stage_hash(vertex),
|
|
107
|
+
"fragment" => stage_hash(fragment)
|
|
108
|
+
},
|
|
109
|
+
"attributes" => vertex_reflection.attributes.map { |a| attribute_hash(a) },
|
|
110
|
+
"uniforms" => merge_uniforms(vertex_reflection.uniforms, fragment_reflection.uniforms),
|
|
111
|
+
"uniform_blocks" => merge_uniform_blocks(vertex_reflection.uniform_blocks, fragment_reflection.uniform_blocks),
|
|
112
|
+
"outputs" => fragment_reflection.outputs.map { |o| output_hash(o) }
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# digestをrequireするのは実際にdigestを計算する経路(build/add_program)
|
|
117
|
+
# だけにするため、ここで遅延require する(M8g)。`glslkit/runtime` 経由で
|
|
118
|
+
# `parse`/`to_h` だけを使う限りこのメソッドは呼ばれず、digestはロードされない。
|
|
119
|
+
def program_digest(vertex_digest, fragment_digest)
|
|
120
|
+
require_relative "digest"
|
|
121
|
+
Digest.hexdigest("#{vertex_digest}:#{fragment_digest}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def stage_hash(stage)
|
|
125
|
+
{"path" => stage.fetch(:path), "digest" => stage.fetch(:source).digest, "url" => stage.fetch(:url)}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def attribute_hash(attribute)
|
|
129
|
+
{"name" => attribute.name, "type" => attribute.type, "location" => attribute.location,
|
|
130
|
+
"array_size" => attribute.array_size}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def output_hash(output)
|
|
134
|
+
{"name" => output.name, "type" => output.type, "location" => output.location}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def merge_uniforms(vertex_uniforms, fragment_uniforms)
|
|
138
|
+
group_by_name(vertex: vertex_uniforms, fragment: fragment_uniforms).map do |name, by_stage|
|
|
139
|
+
first_stage, first_entry = by_stage.first
|
|
140
|
+
by_stage.each do |stage, entry|
|
|
141
|
+
next if entry.type == first_entry.type
|
|
142
|
+
|
|
143
|
+
raise StageMismatchError,
|
|
144
|
+
"uniform #{name.inspect} is #{first_entry.type.inspect} in #{first_stage} " \
|
|
145
|
+
"but #{entry.type.inspect} in #{stage}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
components = Types.components_for(first_entry.type)
|
|
149
|
+
{
|
|
150
|
+
"name" => name,
|
|
151
|
+
"type" => first_entry.type,
|
|
152
|
+
"array_size" => first_entry.array_size,
|
|
153
|
+
"setter" => first_entry.setter,
|
|
154
|
+
"matrix" => first_entry.matrix,
|
|
155
|
+
"sampler" => first_entry.sampler,
|
|
156
|
+
"components" => components,
|
|
157
|
+
"element_count" => components * first_entry.array_size,
|
|
158
|
+
"stages" => by_stage.keys.map(&:to_s)
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def merge_uniform_blocks(vertex_blocks, fragment_blocks)
|
|
164
|
+
group_by_name(vertex: vertex_blocks, fragment: fragment_blocks).map do |name, by_stage|
|
|
165
|
+
first_stage, first_entry = by_stage.first
|
|
166
|
+
by_stage.each do |stage, entry|
|
|
167
|
+
next if entry.layout == first_entry.layout && entry.binding == first_entry.binding
|
|
168
|
+
|
|
169
|
+
raise StageMismatchError, "uniform block #{name.inspect} differs between #{first_stage} and #{stage}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
"name" => name,
|
|
174
|
+
"layout" => first_entry.layout,
|
|
175
|
+
"binding" => first_entry.binding,
|
|
176
|
+
"stages" => by_stage.keys.map(&:to_s)
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# #nameを持つエントリの {vertex: [...], fragment: [...]} を受け取り、
|
|
182
|
+
# 初出順(vertexのエントリが先、fragmentのみの名前はその後)を保った
|
|
183
|
+
# name => {stage => entry} の順序付きHashを返す。
|
|
184
|
+
#
|
|
185
|
+
# 同一ステージのentries内に同名が2件以上あった場合、後のものが前のものを
|
|
186
|
+
# 静かに上書きする(`[stage] = entry`)。これは**入力がValidator(§8.3の
|
|
187
|
+
# E006)で検証済みで、同一ステージ内の重複が存在しないこと**を前提として
|
|
188
|
+
# 許容している。Validatorを経ていない入力でこのメソッドを単体利用すると、
|
|
189
|
+
# 重複が黙って握り潰されるので注意。
|
|
190
|
+
def group_by_name(stage_entries)
|
|
191
|
+
grouped = {}
|
|
192
|
+
stage_entries.each do |stage, entries|
|
|
193
|
+
entries.each { |entry| (grouped[entry.name] ||= {})[stage] = entry }
|
|
194
|
+
end
|
|
195
|
+
grouped
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glslkit
|
|
4
|
+
# コメントと不要な空白のみを除去する — 識別子のリネームも行数のマージも
|
|
5
|
+
# しない。プリプロセッサディレクティブ(#version, #line, #ifdefなど)は
|
|
6
|
+
# それぞれ独立した1物理行でなければならないため、改行は一切削除しない:
|
|
7
|
+
# 入力の各行は必ず出力のちょうど1行に対応するので、コード中に既にある
|
|
8
|
+
# #lineディレクティブの行番号は正しいまま保たれ、Reflectionの抽出結果も
|
|
9
|
+
# 変化しない。
|
|
10
|
+
module Minifier
|
|
11
|
+
COMMENT_PATTERN = %r{//[^\n]*|/\*.*?\*/}m
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def minify(code)
|
|
16
|
+
strip_comments(code).each_line.map { |line| minify_line(line) }.join
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def strip_comments(code)
|
|
20
|
+
code.gsub(COMMENT_PATTERN) do |match|
|
|
21
|
+
newlines = match.count("\n")
|
|
22
|
+
# 改行を含まないコメントでも、区切りは残しておく必要がある。
|
|
23
|
+
# 例えば `vec3/* x */foo` が `vec3foo` になってしまわないように。
|
|
24
|
+
newlines.positive? ? ("\n" * newlines) : " "
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def minify_line(line)
|
|
29
|
+
ending = line.end_with?("\n") ? "\n" : ""
|
|
30
|
+
"#{line.chomp.strip.gsub(/[ \t]+/, " ")}#{ending}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|